3 Commits

464 changed files with 2964 additions and 471 deletions
@@ -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": "<Keyboard>/r",
"interactions": "",
"processors": "",
"groups": ";Keyboard&Mouse",
"action": "BuildRotate",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "b1b01001-0001-0001-0001-000000000001",
-204
View File
@@ -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
@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: d804f7d018e008f4db0ea774eeaadda6
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 2100000
userData:
assetBundleName:
assetBundleVariant:
+17
View File
@@ -430,6 +430,7 @@ GameObject:
- component: {fileID: 4782095384616423459}
- component: {fileID: 1509526206074017255}
- component: {fileID: 1890885114087150018}
- component: {fileID: -4423719270533760289}
- component: {fileID: 656527942023048284}
- component: {fileID: 4969709554801488359}
m_Layer: 10
@@ -919,6 +920,7 @@ MonoBehaviour:
- {fileID: 4232943699575871128}
- {fileID: 7331404443355966726}
- {fileID: 3026700730577618004}
- {fileID: -4423719270533760289}
ownerOnlyObjects:
- {fileID: 3016760143010852029}
- {fileID: 5457574158965716017}
@@ -1040,6 +1042,21 @@ MonoBehaviour:
nameLabel: {fileID: 7210073718532933207}
faceCamera: 1
flipHorizontal: 0
--- !u!114 &-4423719270533760289
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5167716622245296089}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fabb912832c44b945a26c8ef03db222a, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::Ashwild.Crafting.CraftDiscovery
_componentIndexCache: 255
_addedNetworkObject: {fileID: -5889822588553870904}
_networkObjectCache: {fileID: 0}
--- !u!114 &656527942023048284
MonoBehaviour:
m_ObjectHideFlags: 0
+78 -14
View File
@@ -1,5 +1,69 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &4431250198828445795
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 4036155841356705039}
- component: {fileID: 6008748456738279482}
m_Layer: 5
m_Name: GameObject
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &4036155841356705039
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4431250198828445795}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 7818010301368158588}
- {fileID: 3950729247332657127}
m_Father: {fileID: 2991053326735533102}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: -20.8044, y: -20.8044}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &6008748456738279482
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4431250198828445795}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 59f8146938fff824cb5fd77236b75775, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.VerticalLayoutGroup
m_Padding:
m_Left: 0
m_Right: 0
m_Top: 0
m_Bottom: 0
m_ChildAlignment: 4
m_Spacing: 0
m_ChildForceExpandWidth: 1
m_ChildForceExpandHeight: 1
m_ChildControlWidth: 1
m_ChildControlHeight: 0
m_ChildScaleWidth: 1
m_ChildScaleHeight: 1
m_ReverseArrangement: 0
--- !u!1 &4933178774466085049
GameObject:
m_ObjectHideFlags: 0
@@ -25,17 +89,17 @@ RectTransform:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4933178774466085049}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 2991053326735533102}
m_Father: {fileID: 4036155841356705039}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 1}
m_AnchorMax: {x: 0.5, y: 1}
m_AnchoredPosition: {x: 0, y: -10.6}
m_SizeDelta: {x: 126.0229, y: 126.0229}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 128}
m_Pivot: {x: 0.5, y: 1}
--- !u!222 &8806915406871817360
CanvasRenderer:
@@ -101,17 +165,17 @@ RectTransform:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5498742472193566416}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 2991053326735533102}
m_Father: {fileID: 4036155841356705039}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0}
m_AnchorMax: {x: 0.5, y: 0}
m_AnchoredPosition: {x: 0, y: 13.2}
m_SizeDelta: {x: 0, y: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 89.5978, y: -180.48}
m_SizeDelta: {x: 66.59, y: 0}
m_Pivot: {x: 0.5, y: 0}
--- !u!222 &4058813637582350659
CanvasRenderer:
@@ -258,8 +322,7 @@ RectTransform:
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 7818010301368158588}
- {fileID: 3950729247332657127}
- {fileID: 4036155841356705039}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
@@ -319,5 +382,6 @@ MonoBehaviour:
m_EditorClassIdentifier: Assembly-CSharp::IngredientSlotUI
iconImage: {fileID: 6656192589770360378}
quantityText: {fileID: 2097148993551982845}
mysteryIcon: {fileID: 21300000, guid: 674a34f9888b21f4f9e8fe295f753065, type: 3}
enoughColor: {r: 1, g: 1, b: 1, a: 1}
missingColor: {r: 1, g: 0.3, b: 0.3, a: 1}
@@ -191,7 +191,7 @@ RectTransform:
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 26.81, y: -30.848938}
m_SizeDelta: {x: 0, y: 0}
m_SizeDelta: {x: 26.81, y: 0}
m_Pivot: {x: 1, y: 0.5}
--- !u!222 &4956121116561260591
CanvasRenderer:
File diff suppressed because it is too large Load Diff
+101 -100
View File
@@ -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
@@ -3613,22 +3617,6 @@ PrefabInstance:
serializedVersion: 3
m_TransformParent: {fileID: 0}
m_Modifications:
- target: {fileID: 268644269685709644, guid: fc052529108a2854aa09a1d295778ef4, type: 3}
propertyPath: m_AnchorMax.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 268644269685709644, guid: fc052529108a2854aa09a1d295778ef4, type: 3}
propertyPath: m_AnchorMin.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 268644269685709644, guid: fc052529108a2854aa09a1d295778ef4, type: 3}
propertyPath: m_SizeDelta.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 268644269685709644, guid: fc052529108a2854aa09a1d295778ef4, type: 3}
propertyPath: m_AnchoredPosition.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 297657202941871783, guid: fc052529108a2854aa09a1d295778ef4, type: 3}
propertyPath: m_AnchorMax.y
value: 0
@@ -3773,14 +3761,6 @@ PrefabInstance:
propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 1798594070215275997, guid: fc052529108a2854aa09a1d295778ef4, type: 3}
propertyPath: m_SizeDelta.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 1798594070215275997, guid: fc052529108a2854aa09a1d295778ef4, type: 3}
propertyPath: m_SizeDelta.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 1941997771569907896, guid: fc052529108a2854aa09a1d295778ef4, type: 3}
propertyPath: m_AnchorMax.y
value: 0
@@ -3817,34 +3797,6 @@ PrefabInstance:
propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2645420000368753197, guid: fc052529108a2854aa09a1d295778ef4, type: 3}
propertyPath: m_AnchorMax.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2645420000368753197, guid: fc052529108a2854aa09a1d295778ef4, type: 3}
propertyPath: m_AnchorMin.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2645420000368753197, guid: fc052529108a2854aa09a1d295778ef4, type: 3}
propertyPath: m_SizeDelta.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2645420000368753197, guid: fc052529108a2854aa09a1d295778ef4, type: 3}
propertyPath: m_AnchoredPosition.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2645420000368753197, guid: fc052529108a2854aa09a1d295778ef4, type: 3}
propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 3137753552549908057, guid: fc052529108a2854aa09a1d295778ef4, type: 3}
propertyPath: m_SizeDelta.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 3137753552549908057, guid: fc052529108a2854aa09a1d295778ef4, type: 3}
propertyPath: m_SizeDelta.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 3377263676762350218, guid: fc052529108a2854aa09a1d295778ef4, type: 3}
propertyPath: m_AnchorMax.y
value: 0
@@ -4041,26 +3993,6 @@ PrefabInstance:
propertyPath: m_SizeDelta.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 5483805172319020686, guid: fc052529108a2854aa09a1d295778ef4, type: 3}
propertyPath: m_AnchorMax.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 5483805172319020686, guid: fc052529108a2854aa09a1d295778ef4, type: 3}
propertyPath: m_AnchorMin.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 5483805172319020686, guid: fc052529108a2854aa09a1d295778ef4, type: 3}
propertyPath: m_SizeDelta.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 5483805172319020686, guid: fc052529108a2854aa09a1d295778ef4, type: 3}
propertyPath: m_AnchoredPosition.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 5483805172319020686, guid: fc052529108a2854aa09a1d295778ef4, type: 3}
propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 5516694424821111620, guid: fc052529108a2854aa09a1d295778ef4, type: 3}
propertyPath: m_AnchorMax.y
value: 0
@@ -4245,22 +4177,6 @@ PrefabInstance:
propertyPath: m_AnchoredPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 7716463092849739759, guid: fc052529108a2854aa09a1d295778ef4, type: 3}
propertyPath: m_AnchorMax.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 7716463092849739759, guid: fc052529108a2854aa09a1d295778ef4, type: 3}
propertyPath: m_AnchorMin.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 7716463092849739759, guid: fc052529108a2854aa09a1d295778ef4, type: 3}
propertyPath: m_SizeDelta.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 7716463092849739759, guid: fc052529108a2854aa09a1d295778ef4, type: 3}
propertyPath: m_AnchoredPosition.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 7749384703976975436, guid: fc052529108a2854aa09a1d295778ef4, type: 3}
propertyPath: m_AnchorMax.y
value: 0
@@ -4317,18 +4233,6 @@ PrefabInstance:
propertyPath: m_SizeDelta.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 7924365604093564939, guid: fc052529108a2854aa09a1d295778ef4, type: 3}
propertyPath: m_SizeDelta.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 8095760197126531454, guid: fc052529108a2854aa09a1d295778ef4, type: 3}
propertyPath: m_SizeDelta.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 8095760197126531454, guid: fc052529108a2854aa09a1d295778ef4, type: 3}
propertyPath: m_SizeDelta.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 8331890770765351021, guid: fc052529108a2854aa09a1d295778ef4, type: 3}
propertyPath: m_Name
value: Canvas
@@ -5514,6 +5418,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 +6307,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 +29616,4 @@ SceneRoots:
- {fileID: 1836939580}
- {fileID: 1902801754}
- {fileID: 1957565059}
- {fileID: 668733573}
+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
}
}
+60 -1
View File
@@ -99,6 +99,43 @@ namespace Ashwild.Building
#endregion
#region Demolish Build
/// <summary>
/// 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 <see cref="spawned"/> stays aligned with <see cref="builds"/>
/// on every machine. No-op when the object is not one of our committed builds.
/// </summary>
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);
}
/// <summary>
/// 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.
/// </summary>
[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
/// <summary>
@@ -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<BuiltStructure>() == null) go.AddComponent<BuiltStructure>();
return go;
}
/// <summary>
@@ -246,6 +287,24 @@ namespace Ashwild.Building
MarkOccupancy(go);
}
/// <summary>
/// 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.
/// </summary>
private void RecomputeOccupancy()
{
foreach (GameObject go in spawned)
{
if (go == null) continue;
foreach (BuildSnapPoint sp in go.GetComponentsInChildren<BuildSnapPoint>(true))
sp.SetOccupied(false);
}
foreach (GameObject go in spawned)
MarkOccupancy(go);
}
/// <summary>
/// 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
@@ -0,0 +1,91 @@
using UnityEngine;
namespace Ashwild.Building
{
/// <summary>
/// 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.
/// </summary>
[DisallowMultipleComponent]
public class BuiltStructure : MonoBehaviour
{
#region State
/// <summary>
/// Every child renderer, cached so the highlight can swap and restore their materials.
/// </summary>
private Renderer[] renderers;
/// <summary>
/// Each renderer's original materials, kept so the swap is fully reversible.
/// </summary>
private Material[][] baseMaterials;
/// <summary>
/// Whether the demolition material is currently applied, so swaps stay idempotent.
/// </summary>
private bool highlighted;
#endregion
#region Unity Lifecycle
/// <summary>
/// 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.
/// </summary>
private void Awake()
{
renderers = GetComponentsInChildren<Renderer>(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
/// <summary>
/// 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.
/// </summary>
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;
}
}
/// <summary>
/// Restores each renderer's authored materials. No-op when not highlighted.
/// </summary>
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
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: fe38331f524a7db4cb3d53381b9ea244
@@ -0,0 +1,119 @@
using System.Collections.Generic;
using UnityEngine;
using FishNet.Object;
using Ashwild.Inventory;
using Ashwild.Player;
namespace Ashwild.Crafting
{
/// <summary>
/// The local player's craft-discovery knowledge: the set of items it has ever acquired.
/// A recipe is only revealed (and only craftable) once its ingredients have been discovered,
/// so the player uncovers crafts by gathering resources rather than seeing the whole catalog
/// at once. Discovery is triggered the first time an item enters the inventory — pickups,
/// harvest drops AND craft results all count, which lets crafting chains reveal step by step.
///
/// This is owner-only local state: each player keeps its own knowledge, and it is never
/// replicated over the wire (no one else needs to read it). It mirrors PlayerInventory's
/// singleton pattern purely for reliable owner gating — set the Instance for the owning
/// client in OnStartNetwork, and gate the component off on remotes via ownerOnlyBehaviours.
/// </summary>
[DisallowMultipleComponent]
public class CraftDiscovery : NetworkBehaviour
{
#region Singleton
/// <summary>
/// The local player's discovery knowledge (set only on the owning client).
/// </summary>
public static CraftDiscovery Instance { get; private set; }
#endregion
#region State
private readonly HashSet<ItemData> discovered = new HashSet<ItemData>();
#endregion
#region Network Lifecycle
/// <summary>
/// Registers the singleton on the owning client only (before LocalPlayerSpawned fires).
/// </summary>
public override void OnStartNetwork()
{
base.OnStartNetwork();
if (base.Owner.IsLocalClient) Instance = this;
}
/// <summary>
/// Clears the singleton when this player leaves the network.
/// </summary>
public override void OnStopNetwork()
{
base.OnStopNetwork();
if (Instance == this) Instance = null;
}
#endregion
#region Unity Lifecycle
/// <summary>
/// Starts tracking every item the local player acquires.
/// </summary>
private void OnEnable()
{
PlayerEvents.ItemAdded += HandleItemAdded;
}
/// <summary>
/// Stops tracking — mirrors OnEnable exactly.
/// </summary>
private void OnDisable()
{
PlayerEvents.ItemAdded -= HandleItemAdded;
}
#endregion
#region Event Handlers
/// <summary>
/// Marks an acquired item as discovered. Guarded to the owning instance so a remote puppet
/// that briefly subscribed before being gated off can never drive discovery on the wrong copy.
/// </summary>
private void HandleItemAdded(ItemData item, int quantity)
{
if (Instance != this) return;
Discover(item);
}
#endregion
#region Public API
/// <summary>
/// Whether the local player has ever acquired this item (and therefore knows it).
/// </summary>
public bool IsDiscovered(ItemData item) => item != null && discovered.Contains(item);
#endregion
#region Internal Helpers
/// <summary>
/// Adds an item to the discovered set the first time it is seen and announces it on the bus
/// so the crafting view can reveal any recipe the item unlocks.
/// </summary>
private void Discover(ItemData item)
{
if (item == null) return;
if (discovered.Add(item))
PlayerEvents.RaiseItemDiscovered(item);
}
#endregion
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: fabb912832c44b945a26c8ef03db222a
+89 -4
View File
@@ -1,3 +1,4 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using Ashwild.Inventory;
@@ -31,6 +32,7 @@ namespace Ashwild.Crafting
private PlayerInventory inventory;
private bool bound;
private readonly HashSet<CraftingRecipe> unlockedRecipes = new HashSet<CraftingRecipe>();
#endregion
@@ -43,19 +45,25 @@ namespace Ashwild.Crafting
/// <summary>
/// Returns whether the local player can currently craft the recipe the given number of times.
/// A recipe is only craftable once every ingredient has been discovered, on top of the usual
/// inventory check — an ingredient still shown as a mystery ("?") blocks the craft.
/// </summary>
public bool CanCraft(CraftingRecipe recipe, int count = 1)
{
return PlayerInventory.Instance != null && recipe.CanCraft(PlayerInventory.Instance, count);
return PlayerInventory.Instance != null
&& IsFullyDiscovered(recipe)
&& recipe.CanCraft(PlayerInventory.Instance, count);
}
/// <summary>
/// Consumes the ingredients and produces the result if the local player can afford it.
/// Consumes the ingredients and produces the result if the local player can afford it and has
/// discovered every ingredient. The result item is added through the inventory, which in turn
/// discovers it — so crafting an intermediate result reveals the recipes further down the chain.
/// </summary>
public bool TryCraft(CraftingRecipe recipe, int count = 1)
{
PlayerInventory inv = PlayerInventory.Instance;
if (inv == null || !recipe.CanCraft(inv, count))
if (inv == null || !IsFullyDiscovered(recipe) || !recipe.CanCraft(inv, count))
return false;
// Consume ingredients
@@ -82,6 +90,7 @@ namespace Ashwild.Crafting
private void OnEnable()
{
PlayerEvents.LocalPlayerSpawned += HandleLocalPlayerSpawned;
PlayerEvents.ItemDiscovered += HandleItemDiscovered;
}
/// <summary>
@@ -90,6 +99,7 @@ namespace Ashwild.Crafting
private void OnDisable()
{
PlayerEvents.LocalPlayerSpawned -= HandleLocalPlayerSpawned;
PlayerEvents.ItemDiscovered -= HandleItemDiscovered;
}
/// <summary>
@@ -98,7 +108,7 @@ namespace Ashwild.Crafting
private void Start()
{
if (craftingUI != null)
craftingUI.Build(recipes, CanCraft, CountItem, TryCraft);
craftingUI.Build(recipes, CanCraft, CountItem, TryCraft, IsDiscovered, IsRecipeVisible);
else
Debug.LogError("[CraftingManager] No CraftingUI assigned — the crafting panel will not populate.", this);
@@ -134,6 +144,19 @@ namespace Ashwild.Crafting
craftingUI.Refresh();
}
/// <summary>
/// Reacts to a newly discovered item: announces any recipe it just unlocked (for the "NEW"
/// craft notification), then refreshes the view so those recipes appear and their once-mystery
/// ingredients are shown.
/// </summary>
private void HandleItemDiscovered(ItemData item)
{
NotifyNewlyUnlockedRecipes();
if (craftingUI != null)
craftingUI.Refresh();
}
#endregion
#region Internal Helpers
@@ -150,10 +173,41 @@ namespace Ashwild.Crafting
inventory.onSlotChanged.AddListener(HandleInventoryChanged);
SeedUnlockedRecipes();
if (craftingUI != null)
craftingUI.Refresh();
}
/// <summary>
/// Records the recipes already revealed at bind time (e.g. from restored discoveries) so they
/// are treated as known and never fire a spurious "NEW" notification. With a fresh player this
/// is empty since discovery starts at zero.
/// </summary>
private void SeedUnlockedRecipes()
{
for (int i = 0; i < recipes.Length; i++)
if (IsRecipeVisible(recipes[i]))
unlockedRecipes.Add(recipes[i]);
}
/// <summary>
/// Raises a "NEW" notification for every recipe that has just become visible (its first
/// discovered ingredient), each announced once. Called after an item is discovered.
/// </summary>
private void NotifyNewlyUnlockedRecipes()
{
for (int i = 0; i < recipes.Length; i++)
{
CraftingRecipe recipe = recipes[i];
if (unlockedRecipes.Contains(recipe)) continue;
if (!IsRecipeVisible(recipe)) continue;
unlockedRecipes.Add(recipe);
PlayerEvents.RaiseRecipeDiscovered(recipe.Icon, recipe.RecipeName);
}
}
/// <summary>
/// How many of an item the local player currently holds (0 when offline).
/// </summary>
@@ -162,6 +216,37 @@ namespace Ashwild.Crafting
return PlayerInventory.Instance != null ? PlayerInventory.Instance.CountItem(item) : 0;
}
/// <summary>
/// Whether the local player has discovered the item (false when the discovery tracker isn't ready).
/// </summary>
private bool IsDiscovered(ItemData item)
{
return CraftDiscovery.Instance != null && CraftDiscovery.Instance.IsDiscovered(item);
}
/// <summary>
/// Whether the recipe should appear at all: revealed as soon as at least one of its ingredients
/// has been discovered. Undiscovered ingredients then show as a mystery until found.
/// </summary>
private bool IsRecipeVisible(CraftingRecipe recipe)
{
CraftingIngredient[] ingredients = recipe.Ingredients;
for (int i = 0; i < ingredients.Length; i++)
if (IsDiscovered(ingredients[i].item)) return true;
return false;
}
/// <summary>
/// Whether every ingredient of the recipe has been discovered — the gate for actually crafting it.
/// </summary>
private bool IsFullyDiscovered(CraftingRecipe recipe)
{
CraftingIngredient[] ingredients = recipe.Ingredients;
for (int i = 0; i < ingredients.Length; i++)
if (!IsDiscovered(ingredients[i].item)) return false;
return true;
}
#endregion
}
}
+26 -8
View File
@@ -42,6 +42,8 @@ namespace Ashwild.Crafting
private Func<CraftingRecipe, int, bool> canCraftQuery;
private Func<ItemData, int> countItemQuery;
private Func<CraftingRecipe, int, bool> craftRequest;
private Func<ItemData, bool> isDiscoveredQuery;
private Func<CraftingRecipe, bool> isRecipeVisibleQuery;
private readonly List<RecipeButtonUI> recipeButtons = new List<RecipeButtonUI>();
private readonly List<GameObject> ingredientInstances = new List<GameObject>();
private RecipeButtonUI selectedButton;
@@ -55,12 +57,15 @@ namespace Ashwild.Crafting
/// <summary>
/// Builds the recipe buttons (once) and wires the manager-owned queries/action. Safe to
/// call again later to re-point the delegates; it then simply refreshes the display.
/// The discovery queries decide which recipes are shown and which ingredients are still a mystery.
/// </summary>
public void Build(CraftingRecipe[] recipes, Func<CraftingRecipe, int, bool> canCraft, Func<ItemData, int> countItem, Func<CraftingRecipe, int, bool> craft)
public void Build(CraftingRecipe[] recipes, Func<CraftingRecipe, int, bool> canCraft, Func<ItemData, int> countItem, Func<CraftingRecipe, int, bool> craft, Func<ItemData, bool> isDiscovered, Func<CraftingRecipe, bool> isRecipeVisible)
{
canCraftQuery = canCraft;
countItemQuery = countItem;
craftRequest = craft;
isDiscoveredQuery = isDiscovered;
isRecipeVisibleQuery = isRecipeVisible;
if (!built)
{
@@ -81,8 +86,9 @@ namespace Ashwild.Crafting
}
/// <summary>
/// Recomputes craftability for every recipe button and refreshes the open detail.
/// Called by the manager whenever the local player's inventory changes.
/// Recomputes each recipe button's visibility (revealed once an ingredient is discovered) and
/// its craftability, then refreshes the open detail. Called by the manager whenever the local
/// player's inventory changes or a new item is discovered.
/// </summary>
public void Refresh()
{
@@ -91,6 +97,9 @@ namespace Ashwild.Crafting
for (int i = 0; i < recipeButtons.Count; i++)
{
RecipeButtonUI btn = recipeButtons[i];
bool visible = isRecipeVisibleQuery == null || isRecipeVisibleQuery(btn.Recipe);
btn.gameObject.SetActive(visible);
bool canCraft = canCraftQuery != null && canCraftQuery(btn.Recipe, btn.CraftCount);
btn.UpdateCraftability(canCraft);
}
@@ -193,12 +202,21 @@ namespace Ashwild.Crafting
GameObject slotGO = Instantiate(ingredientSlotPrefab, ingredientContainer);
IngredientSlotUI slotUI = slotGO.GetComponent<IngredientSlotUI>();
int required = ingredients[i].quantity * count;
int playerHas = countItemQuery != null ? countItemQuery(ingredients[i].item) : 0;
slotUI.Setup(ingredients[i].item, required, playerHas);
if (playerHas < required)
bool discovered = isDiscoveredQuery == null || isDiscoveredQuery(ingredients[i].item);
if (!discovered)
{
slotUI.SetupMystery();
canCraft = false;
}
else
{
int required = ingredients[i].quantity * count;
int playerHas = countItemQuery != null ? countItemQuery(ingredients[i].item) : 0;
slotUI.Setup(ingredients[i].item, required, playerHas);
if (playerHas < required)
canCraft = false;
}
ingredientInstances.Add(slotGO);
}
@@ -5,21 +5,45 @@ using Ashwild.Inventory;
namespace Ashwild.Crafting
{
/// <summary>
/// A single ingredient cell in the crafting detail panel. Renders either a known ingredient
/// (icon + required quantity, tinted by whether the player has enough) or a mystery placeholder
/// ("?") for an ingredient the player has not discovered yet.
/// </summary>
public class IngredientSlotUI : MonoBehaviour
{
[SerializeField] private Image iconImage;
[SerializeField] private TextMeshProUGUI quantityText;
[Header("Mystery (undiscovered ingredient)")]
[Tooltip("Icon shown for an ingredient the player hasn't discovered yet (a question mark).")]
[SerializeField] private Sprite mysteryIcon;
[Header("Colors")]
[SerializeField] private Color enoughColor = Color.white;
[SerializeField] private Color missingColor = new Color(1f, 0.3f, 0.3f, 1f);
/// <summary>
/// Renders a known ingredient: its icon and required amount, dimmed/red when the player lacks it.
/// </summary>
public void Setup(ItemData item, int required, int playerHas)
{
quantityText.gameObject.SetActive(true);
iconImage.sprite = item.Icon;
quantityText.text = "x" + required;
quantityText.color = playerHas >= required ? enoughColor : missingColor;
iconImage.color = playerHas >= required ? Color.white : new Color(1f, 1f, 1f, 0.5f);
}
/// <summary>
/// Renders an undiscovered ingredient as a mystery: the "?" icon with no quantity text, so the
/// player knows a craft exists but must still find what it needs and how much.
/// </summary>
public void SetupMystery()
{
iconImage.sprite = mysteryIcon;
iconImage.color = new Color(1f, 1f, 1f, 0.5f);
quantityText.gameObject.SetActive(false);
}
}
}
+39 -1
View File
@@ -24,6 +24,10 @@ namespace Ashwild.Inventory
[Header("Context Menu")]
[SerializeField] private SlotContextMenu contextMenu;
[Header("Hover Description")]
[Tooltip("Same panel used by the inventory — assign the shared HoverDescriptionUI here too.")]
[SerializeField] private HoverDescriptionUI hoverDescription;
private PlayerInventory inventory;
private Tweener scaleTween;
private Tweener alphaTween;
@@ -51,7 +55,7 @@ namespace Ashwild.Inventory
{
// Visual setup that does not need the player.
for (int i = 0; i < hotbarSlot.Length; i++)
hotbarSlot[i].Initialize(i, InventoryUI.OnSwapRequested, OnSlotClicked);
hotbarSlot[i].Initialize(i, InventoryUI.OnSwapRequested, OnSlotClicked, OnSlotHoverEnter, OnSlotHoverExit);
transform.localScale = Vector3.one * idleScale;
if (canvasGroup != null)
@@ -167,6 +171,40 @@ namespace Ashwild.Inventory
hotbarSlot[index].UpdateVisual(inventory.GetSlot(index));
}
/// <summary>
/// Shows the shared description panel for a hovered hotbar slot, but only while the inventory
/// is open — during normal play the hotbar must not pop a description. The duration bar
/// reflects the item's remaining uses/durability.
/// </summary>
private void OnSlotHoverEnter(int index)
{
if (hoverDescription == null || inventory == null || !PlayerEvents.IsInventoryOpen) return;
InventorySlot slot = inventory.GetSlot(index);
if (slot == null || slot.IsEmpty)
{
hoverDescription.Hide();
return;
}
ItemData item = slot.ItemData;
bool hasDuration = item.HasUses;
float fill = hasDuration ? (float)slot.CurrentUses / item.MaxUses : 0f;
string durationText = hasDuration ? $"{slot.CurrentUses} / {item.MaxUses}" : string.Empty;
hoverDescription.Show(new ItemDescriptionView(
item.Icon, item.ItemName, item.Description, hasDuration, fill, durationText));
}
/// <summary>
/// Hides the description panel when the pointer leaves a hotbar slot.
/// </summary>
private void OnSlotHoverExit()
{
if (hoverDescription != null)
hoverDescription.Hide();
}
private void UpdateSelection(int selectedIndex)
{
for (int i = 0; i < hotbarSlot.Length; i++)
@@ -0,0 +1,110 @@
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using DG.Tweening;
namespace Ashwild.Inventory
{
/// <summary>
/// The fixed description panel shown beside the inventory — a pure view. When the pointer
/// enters a filled slot the inventory manager hands it an <see cref="ItemDescriptionView"/> and
/// it fades its CanvasGroup in, drawing the icon, name, description and (for uses-tracked items)
/// the duration bar. It never reads ItemData or the inventory itself; it renders what it is given.
/// </summary>
[DisallowMultipleComponent]
public class HoverDescriptionUI : MonoBehaviour
{
#region Serialized Fields
[Header("References")]
[SerializeField] private CanvasGroup canvasGroup;
[SerializeField] private Image iconImage;
[SerializeField] private TextMeshProUGUI nameText;
[SerializeField] private TextMeshProUGUI descriptionText;
[Header("Duration Bar")]
[Tooltip("Root object of the duration/uses bar — shown only for items that track uses.")]
[SerializeField] private GameObject durationBarRoot;
[Tooltip("Filled image whose fillAmount maps to the remaining uses (Image Type = Filled).")]
[SerializeField] private Image durationBarFill;
[Tooltip("Label drawn over the bar, e.g. \"3 / 5\".")]
[SerializeField] private TextMeshProUGUI durationText;
[Header("Animation")]
[SerializeField] private float fadeDuration = 0.15f;
#endregion
#region State
private Tweener fadeTween;
#endregion
#region Unity Lifecycle
/// <summary>
/// Starts hidden so the panel only appears once a slot is actually hovered.
/// </summary>
private void Awake() => SetHidden();
/// <summary>
/// Kills the fade tween so it never targets the CanvasGroup after teardown.
/// </summary>
private void OnDestroy() => fadeTween?.Kill();
#endregion
#region Public API
/// <summary>
/// Draws the payload and fades the panel in. Called by the inventory manager on hover enter.
/// </summary>
public void Show(ItemDescriptionView view)
{
if (iconImage != null)
{
iconImage.sprite = view.Icon;
iconImage.enabled = view.Icon != null;
}
if (nameText != null)
nameText.text = view.Name;
if (descriptionText != null)
descriptionText.text = view.Description;
if (durationBarRoot != null)
durationBarRoot.SetActive(view.HasDuration);
if (view.HasDuration)
{
if (durationBarFill != null)
durationBarFill.fillAmount = view.DurationFill;
if (durationText != null)
durationText.text = view.DurationText;
}
fadeTween?.Kill();
fadeTween = canvasGroup.DOFade(1f, fadeDuration).SetUpdate(true);
}
/// <summary>
/// Fades the panel out via its CanvasGroup only — the GameObject stays active so it can be
/// shown again instantly. Called on hover exit / inventory close.
/// </summary>
public void Hide()
{
fadeTween?.Kill();
fadeTween = canvasGroup.DOFade(0f, fadeDuration).SetUpdate(true);
}
#endregion
#region Internal Helpers
/// <summary>
/// Instantly parks the panel invisible via the CanvasGroup, without deactivating it.
/// </summary>
private void SetHidden() => canvasGroup.alpha = 0f;
#endregion
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 40050a905c9108244901dadab24daa28
+40 -1
View File
@@ -33,6 +33,9 @@ namespace Ashwild.Inventory
[Header("Context Menu")]
[SerializeField] private SlotContextMenu contextMenu;
[Header("Hover Description")]
[SerializeField] private HoverDescriptionUI hoverDescription;
[Header("Categories")]
[SerializeField] private InventoryCategoryManager categoryManager;
@@ -93,7 +96,7 @@ namespace Ashwild.Inventory
int slotIndex = inventory.HotbarSize + i;
GameObject slotGO = Instantiate(slotPrefab, slotContainer);
SlotUI slotUI = slotGO.GetComponent<SlotUI>();
slotUI.Initialize(slotIndex, OnSwapRequested, OnSlotClicked);
slotUI.Initialize(slotIndex, OnSwapRequested, OnSlotClicked, OnSlotHoverEnter, OnSlotHoverExit);
slotUIs[i] = slotUI;
}
@@ -133,6 +136,9 @@ namespace Ashwild.Inventory
if (contextMenu != null)
contextMenu.Hide();
if (hoverDescription != null)
hoverDescription.Hide();
if (categoryManager != null)
categoryManager.Close();
@@ -192,6 +198,39 @@ namespace Ashwild.Inventory
}
}
/// <summary>
/// Builds the description payload for the hovered slot and shows the side panel. Empty slots
/// keep the panel hidden. The duration bar reflects the item's remaining uses/durability.
/// </summary>
private void OnSlotHoverEnter(int index)
{
if (hoverDescription == null || inventory == null) return;
InventorySlot slot = inventory.GetSlot(index);
if (slot == null || slot.IsEmpty)
{
hoverDescription.Hide();
return;
}
ItemData item = slot.ItemData;
bool hasDuration = item.HasUses;
float fill = hasDuration ? (float)slot.CurrentUses / item.MaxUses : 0f;
string durationText = hasDuration ? $"{slot.CurrentUses} / {item.MaxUses}" : string.Empty;
hoverDescription.Show(new ItemDescriptionView(
item.Icon, item.ItemName, item.Description, hasDuration, fill, durationText));
}
/// <summary>
/// Hides the description panel when the pointer leaves a slot.
/// </summary>
private void OnSlotHoverExit()
{
if (hoverDescription != null)
hoverDescription.Hide();
}
private void RefreshSlot(int index)
{
if (slotUIs == null) return;
@@ -0,0 +1,43 @@
using UnityEngine;
namespace Ashwild.Inventory
{
/// <summary>
/// The view payload for the hover description panel: exactly what the panel renders (icon,
/// name, description, and an optional duration/uses bar), nothing more. The inventory manager
/// builds one of these from a hovered slot and pushes it into the view, so the panel never
/// touches ItemData or the inventory model — it just draws what it is handed.
/// </summary>
public readonly struct ItemDescriptionView
{
public readonly Sprite Icon;
public readonly string Name;
public readonly string Description;
/// <summary>
/// Whether the hovered item tracks uses/durability and should show its duration bar.
/// </summary>
public readonly bool HasDuration;
/// <summary>
/// Remaining uses as a 0..1 fill for the bar's Filled image.
/// </summary>
public readonly float DurationFill;
/// <summary>
/// The remaining/max label drawn over the bar, e.g. "3 / 5".
/// </summary>
public readonly string DurationText;
public ItemDescriptionView(Sprite icon, string name, string description,
bool hasDuration, float durationFill, string durationText)
{
Icon = icon;
Name = name;
Description = description;
HasDuration = hasDuration;
DurationFill = durationFill;
DurationText = durationText;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b901ece22e48ee344837ad720d6f2e18
+25 -2
View File
@@ -6,7 +6,7 @@ using System;
namespace Ashwild.Inventory
{
public class SlotUI : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler, IDropHandler, IPointerClickHandler
public class SlotUI : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler, IDropHandler, IPointerClickHandler, IPointerEnterHandler, IPointerExitHandler
{
[Header("References")]
[SerializeField] private Image iconImage;
@@ -26,6 +26,8 @@ namespace Ashwild.Inventory
private int slotIndex;
private Action<int, int> onSwapRequested;
private Action<int, bool> onClicked;
private Action<int> onHoverEnter;
private Action onHoverExit;
public int SlotIndex => slotIndex;
@@ -43,11 +45,14 @@ namespace Ashwild.Inventory
ghostObject.SetActive(false);
}
public void Initialize(int index, Action<int, int> swapCallback, Action<int, bool> clickCallback)
public void Initialize(int index, Action<int, int> swapCallback, Action<int, bool> clickCallback,
Action<int> hoverEnterCallback = null, Action hoverExitCallback = null)
{
slotIndex = index;
onSwapRequested = swapCallback;
onClicked = clickCallback;
onHoverEnter = hoverEnterCallback;
onHoverExit = hoverExitCallback;
}
/// <summary>
@@ -170,5 +175,23 @@ namespace Ashwild.Inventory
if (eventData.button == PointerEventData.InputButton.Right)
onClicked?.Invoke(slotIndex, true);
}
/// <summary>
/// Reports the hovered slot index so the manager can show its description panel. Suppressed
/// while a drag is in progress, where the panel would only get in the way.
/// </summary>
public void OnPointerEnter(PointerEventData eventData)
{
if (draggedSlot != null) return;
onHoverEnter?.Invoke(slotIndex);
}
/// <summary>
/// Reports that the pointer left the slot so the manager can hide the description panel.
/// </summary>
public void OnPointerExit(PointerEventData eventData)
{
onHoverExit?.Invoke();
}
}
}
@@ -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<bool> 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<int> HotbarSlotPressed;
public static event Action<float> 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
// ============================================================
@@ -101,6 +105,8 @@ namespace Ashwild.Player
public static event Action<ItemData, int> ItemDropped;
public static event Action<ItemData> ItemConsumed;
public static event Action<ItemData, int> ItemPickedUp;
public static event Action<ItemData> ItemDiscovered; // an item entered the inventory for the first time (unlocks recipes)
public static event Action<Sprite, string> RecipeDiscovered; // a recipe was just revealed (icon, name) — for the "NEW" craft notification
public static event Action<ItemData> HeldItemChanged;
public static event Action<bool> InventoryOpenChanged;
public static event Action<bool> ChestOpenChanged;
@@ -128,6 +134,7 @@ namespace Ashwild.Player
public static event Action BuildMenuToggleRequested; // the build hammer asks to open/close the construction menu
public static event Action<bool> BuildMenuOpenChanged; // the construction menu opened (true) or closed (false)
public static event Action<bool> BuildPlacingChanged; // a build ghost started (true) / stopped (false) being positioned
public static event Action<bool> DemolishModeChanged; // the hammer entered (true) / left (false) demolition mode (right-click held)
// ============================================================
// Cooking events
@@ -182,11 +189,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)
@@ -248,6 +257,8 @@ namespace Ashwild.Player
public static void RaiseItemDropped(ItemData d, int q) { Log(nameof(ItemDropped)); ItemDropped?.Invoke(d, q); }
public static void RaiseItemConsumed(ItemData d) { Log(nameof(ItemConsumed)); ItemConsumed?.Invoke(d); }
public static void RaiseItemPickedUp(ItemData d, int q) { Log(nameof(ItemPickedUp)); ItemPickedUp?.Invoke(d, q); }
public static void RaiseItemDiscovered(ItemData d) { Log(nameof(ItemDiscovered)); ItemDiscovered?.Invoke(d); }
public static void RaiseRecipeDiscovered(Sprite icon, string recipeName) { Log(nameof(RecipeDiscovered)); RecipeDiscovered?.Invoke(icon, recipeName); }
public static void RaiseHeldItemChanged(ItemData d) { Log(nameof(HeldItemChanged)); HeldItemChanged?.Invoke(d); }
public static void RaiseInventoryOpenChanged(bool b)
{
@@ -294,6 +305,17 @@ namespace Ashwild.Player
BuildPlacingChanged?.Invoke(placing);
}
/// <summary>
/// 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.
/// </summary>
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 +398,7 @@ namespace Ashwild.Player
IsChestOpen = false;
IsBuildMenuOpen = false;
IsPlacingBuild = false;
IsDemolishing = false;
IsPaused = false;
HoveredInteractable = null;
}
@@ -4,21 +4,28 @@ using Ashwild.Inventory;
namespace Ashwild.Player
{
/// <summary>
/// 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 <b>tap</b> opens/closes the
/// construction menu through the bus, while <b>holding</b> 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.
/// </summary>
[DisallowMultipleComponent]
public class BuildHammerBehaviour : MonoBehaviour, IHeldItemBehaviour
{
#region Serialized Fields
[Header("Cooldown")]
[Header("Gestures")]
/// <summary>
/// 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.
/// </summary>
[SerializeField] private float holdThreshold = 0.2f;
/// <summary>
/// Minimum time, in seconds, between two menu toggles so a burst of taps does not flicker the
/// menu open and shut.
/// </summary>
[SerializeField] private float toggleCooldown = 0.25f;
@@ -31,24 +38,58 @@ namespace Ashwild.Player
/// </summary>
private float nextToggleTime;
/// <summary>
/// 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.
/// </summary>
private bool secondaryDown;
/// <summary>
/// Time, in seconds, the current accepted press began — measured against <see cref="holdThreshold"/>.
/// </summary>
private float secondaryDownTime;
/// <summary>
/// True while the hold has crossed into demolition mode, so the release ends the mode rather
/// than toggling the menu.
/// </summary>
private bool demoActive;
#endregion
#region Unity Lifecycle
/// <summary>
/// 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.
/// </summary>
private void OnEnable()
{
PlayerEvents.SecondaryUsePressed += HandleSecondaryUse;
PlayerEvents.SecondaryUseHeld += HandleSecondaryUse;
}
/// <summary>
/// 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.
/// </summary>
private void OnDisable()
{
PlayerEvents.SecondaryUsePressed -= HandleSecondaryUse;
PlayerEvents.SecondaryUseHeld -= HandleSecondaryUse;
if (demoActive) ExitDemolition();
secondaryDown = false;
}
/// <summary>
/// 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.
/// </summary>
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
/// <summary>
/// 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.
/// </summary>
public void Setup(HeldItemContext context, ItemData item)
{
@@ -68,20 +109,60 @@ namespace Ashwild.Player
#region Event Handlers
/// <summary>
/// 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.
/// </summary>
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
/// <summary>
/// Requests the construction menu to open/close, gated by the toggle cooldown.
/// </summary>
private void ToggleMenu()
{
if (Time.time < nextToggleTime) return;
nextToggleTime = Time.time + toggleCooldown;
PlayerEvents.RaiseBuildMenuToggleRequested();
}
/// <summary>
/// Enters demolition mode and announces it on the bus.
/// </summary>
private void EnterDemolition()
{
demoActive = true;
PlayerEvents.RaiseDemolishModeChanged(true);
}
/// <summary>
/// Leaves demolition mode and announces it on the bus.
/// </summary>
private void ExitDemolition()
{
demoActive = false;
PlayerEvents.RaiseDemolishModeChanged(false);
}
#endregion
}
}
@@ -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++)
{
@@ -19,6 +19,7 @@ namespace Ashwild.UI
private int totalQuantity;
private float hideTimer;
private bool isHiding;
private bool isDiscovery;
private Tweener fadeTween;
private Tweener slideTween;
@@ -30,6 +31,9 @@ namespace Ashwild.UI
// A notification is either a gain (+) or a loss (-); merging only happens within the same direction.
public bool IsGain => totalQuantity >= 0;
// A discovery notification (icon + name + "NEW" badge) never merges with a gain/loss one.
public bool IsDiscovery => isDiscovery;
public void Initialize(ItemData item, int signedQuantity, Color numberColor, float displayDuration, float slideInDuration, Ease slideInEase)
{
itemData = item;
@@ -49,6 +53,34 @@ namespace Ashwild.UI
fadeTween = canvasGroup.DOFade(1f, slideInDuration).SetEase(Ease.OutQuad);
}
/// <summary>
/// Sets up a discovery notification: an icon and name (of the unlocked recipe) with a fixed
/// badge (e.g. "NEW") in the number slot, tinted with the caller-chosen colour. It carries no
/// quantity and never merges, so it reads as a one-time "recipe unlocked" cue.
/// </summary>
public void InitializeDiscovery(Sprite icon, string displayName, string badgeText, Color badgeColor, float displayDuration, float slideInDuration)
{
itemData = null;
isDiscovery = true;
hideTimer = displayDuration;
isHiding = false;
if (iconImage != null)
iconImage.sprite = icon;
if (labelText != null)
labelText.text = displayName;
if (numberText != null)
{
numberText.text = badgeText;
numberText.color = badgeColor;
}
canvasGroup.alpha = 0f;
fadeTween = canvasGroup.DOFade(1f, slideInDuration).SetEase(Ease.OutQuad);
}
public void AddQuantity(int signedAmount, float displayDuration)
{
totalQuantity += signedAmount;
+33 -1
View File
@@ -30,6 +30,11 @@ namespace Ashwild.UI
[SerializeField] private Color gainColor = new Color(0.45f, 1f, 0.45f); // picked up / added
[SerializeField] private Color lossColor = new Color(1f, 0.45f, 0.45f); // dropped / consumed
[Header("Discovery")]
[Tooltip("Badge shown in the number slot when a new item/recipe is discovered.")]
[SerializeField] private string discoveryLabel = "NEW";
[SerializeField] private Color discoveryColor = new Color(1f, 0.85f, 0.3f);
private readonly List<NotificationItemUI> activeNotifications = new List<NotificationItemUI>();
private void OnEnable()
@@ -37,6 +42,7 @@ namespace Ashwild.UI
PlayerEvents.ItemAdded += OnItemAdded;
PlayerEvents.ItemDropped += OnItemDropped;
PlayerEvents.ItemConsumed += OnItemConsumed;
PlayerEvents.RecipeDiscovered += OnRecipeDiscovered;
PlayerEvents.FoodPlacedToCook += OnItemSpent;
PlayerEvents.FuelAdded += OnItemSpent;
}
@@ -46,6 +52,7 @@ namespace Ashwild.UI
PlayerEvents.ItemAdded -= OnItemAdded;
PlayerEvents.ItemDropped -= OnItemDropped;
PlayerEvents.ItemConsumed -= OnItemConsumed;
PlayerEvents.RecipeDiscovered -= OnRecipeDiscovered;
PlayerEvents.FoodPlacedToCook -= OnItemSpent;
PlayerEvents.FuelAdded -= OnItemSpent;
}
@@ -53,6 +60,7 @@ namespace Ashwild.UI
private void OnItemAdded(ItemData item, int quantity) => Push(item, quantity);
private void OnItemDropped(ItemData item, int quantity) => Push(item, -quantity);
private void OnItemConsumed(ItemData item) => Push(item, -1);
private void OnRecipeDiscovered(Sprite icon, string recipeName) => PushDiscovery(icon, recipeName);
// Food placed on a cooking station or fuel fed to it both leave the inventory by one.
private void OnItemSpent(ItemData item) => Push(item, -1);
@@ -69,7 +77,8 @@ namespace Ashwild.UI
// Merge only with a notification for the same item AND same direction
for (int i = 0; i < activeNotifications.Count; i++)
{
if (activeNotifications[i] != null && activeNotifications[i].ItemData == item
if (activeNotifications[i] != null && !activeNotifications[i].IsDiscovery
&& activeNotifications[i].ItemData == item
&& activeNotifications[i].IsGain == gain)
{
activeNotifications[i].AddQuantity(signedQuantity, displayDuration);
@@ -94,6 +103,29 @@ namespace Ashwild.UI
activeNotifications.Add(notification);
}
/// <summary>
/// Pushes a one-time "NEW" notification for a newly unlocked recipe: its result icon and name
/// with the configured badge in the discovery colour. Never merges with anything.
/// </summary>
private void PushDiscovery(Sprite icon, string recipeName)
{
activeNotifications.RemoveAll(n => n == null);
GameObject go = Instantiate(notificationPrefab, container);
NotificationItemUI notification = go.GetComponent<NotificationItemUI>();
float targetY = startOffsetY + activeNotifications.Count * (notificationHeight + spacing);
RectTransform rt = notification.RectTransform;
rt.anchoredPosition = new Vector2(0f, targetY - 30f);
notification.InitializeDiscovery(icon, recipeName, discoveryLabel, discoveryColor, displayDuration, slideInDuration);
rt.DOAnchorPosY(targetY, slideInDuration).SetEase(slideInEase);
activeNotifications.Add(notification);
}
private void LateUpdate()
{
// Clean up destroyed notifications and reposition remaining ones

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Before

Width:  |  Height:  |  Size: 497 KiB

After

Width:  |  Height:  |  Size: 497 KiB

Before

Width:  |  Height:  |  Size: 180 KiB

After

Width:  |  Height:  |  Size: 180 KiB

Before

Width:  |  Height:  |  Size: 214 KiB

After

Width:  |  Height:  |  Size: 214 KiB

Before

Width:  |  Height:  |  Size: 301 KiB

After

Width:  |  Height:  |  Size: 301 KiB

Before

Width:  |  Height:  |  Size: 166 KiB

After

Width:  |  Height:  |  Size: 166 KiB

Before

Width:  |  Height:  |  Size: 236 KiB

After

Width:  |  Height:  |  Size: 236 KiB

Before

Width:  |  Height:  |  Size: 273 KiB

After

Width:  |  Height:  |  Size: 273 KiB

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Before

Width:  |  Height:  |  Size: 388 KiB

After

Width:  |  Height:  |  Size: 388 KiB

Before

Width:  |  Height:  |  Size: 300 KiB

After

Width:  |  Height:  |  Size: 300 KiB

Before

Width:  |  Height:  |  Size: 301 KiB

After

Width:  |  Height:  |  Size: 301 KiB

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Some files were not shown because too many files have changed in this diff Show More