added inventory and stamina system

This commit is contained in:
2026-02-17 15:38:46 +00:00
parent 8b975163a0
commit 27f2931ce1
43 changed files with 2980 additions and 250 deletions

8
Assets/Items.meta Normal file
View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 94c8f7f884ccd0e47a66f83dc8fe8b0e
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,20 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
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: b549159f64e1b164082e2e4ae4d28ac1, type: 3}
m_Name: BhopBoots_Item
m_EditorClassIdentifier: Assembly-CSharp::ItemDefinition
itemName: Bunny Hop Boots
icon: {fileID: 0}
type: 0
weaponPrefab: {fileID: 0}
isEquippable: 1
description: Illegal footwear. Gives 200 stamina while worn. Do not wear near cliffs.

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e09f7b05a155ba8449a0f0d8412ebe9d
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,19 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
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: b549159f64e1b164082e2e4ae4d28ac1, type: 3}
m_Name: GunSplat_Item
m_EditorClassIdentifier: Assembly-CSharp::ItemDefinition
itemName: Gun Splat
icon: {fileID: 0}
type: 2
weaponPrefab: {fileID: 8985654986098955209, guid: aa9cce2c129fa1a41bf5a539b136f7a6, type: 3}
description: A janky lidar-scanned sidearm. Shoots first, looks weird always.

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 2b5a641a42945604c9ffa88754a9f6d1
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,5 +1,18 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &-809172725653918854
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
--- !u!21 &2100000
Material:
serializedVersion: 8
@@ -30,8 +43,22 @@ Material:
m_Texture: {fileID: -5390149515647955136, guid: 24bb6e32322f940e59304941b7849582, type: 3}
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:
- _AlphaClip: 0
- _AlphaToMask: 0
- _BUILTIN_AlphaClip: 0
- _BUILTIN_Blend: 0
- _BUILTIN_CullMode: 2
@@ -43,6 +70,19 @@ Material:
- _BUILTIN_ZTest: 4
- _BUILTIN_ZWrite: 1
- _BUILTIN_ZWriteControl: 0
- _Blend: 0
- _CastShadows: 1
- _Cull: 2
- _DstBlend: 0
- _DstBlendAlpha: 0
- _QueueControl: 1
- _QueueOffset: 0
- _SrcBlend: 1
- _SrcBlendAlpha: 1
- _Surface: 0
- _ZTest: 4
- _ZWrite: 1
- _ZWriteControl: 0
- alphaCutoff: 0
- baseColorTexture_texCoord: 0
m_Colors:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c47b29375f392f14385951a3f2e89a56
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,161 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &976112319745431805
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 4275644691617312389}
- component: {fileID: 5061255202814967808}
- component: {fileID: 1990460956270860801}
m_Layer: 0
m_Name: Visual
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &4275644691617312389
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 976112319745431805}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 0.4, y: 0.2, z: 0.7}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 9221872812818225285}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!33 &5061255202814967808
MeshFilter:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 976112319745431805}
m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0}
--- !u!23 &1990460956270860801
MeshRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 976112319745431805}
m_Enabled: 1
m_CastShadows: 1
m_ReceiveShadows: 1
m_DynamicOccludee: 1
m_StaticShadowCaster: 0
m_MotionVectors: 1
m_LightProbeUsage: 1
m_ReflectionProbeUsage: 1
m_RayTracingMode: 2
m_RayTraceProcedural: 0
m_RayTracingAccelStructBuildFlagsOverride: 0
m_RayTracingAccelStructBuildFlags: 1
m_SmallMeshCulling: 1
m_ForceMeshLod: -1
m_MeshLodSelectionBias: 0
m_RenderingLayerMask: 1
m_RendererPriority: 0
m_Materials:
- {fileID: 0}
m_StaticBatchInfo:
firstSubMesh: 0
subMeshCount: 0
m_StaticBatchRoot: {fileID: 0}
m_ProbeAnchor: {fileID: 0}
m_LightProbeVolumeOverride: {fileID: 0}
m_ScaleInLightmap: 1
m_ReceiveGI: 1
m_PreserveUVs: 1
m_IgnoreNormalsForChartDetection: 0
m_ImportantGI: 0
m_StitchLightmapSeams: 1
m_SelectedEditorRenderState: 3
m_MinimumChartSize: 4
m_AutoUVMaxDistance: 0.5
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_GlobalIlluminationMeshLod: 0
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingOrder: 0
m_MaskInteraction: 0
m_AdditionalVertexStreams: {fileID: 0}
--- !u!1 &5967924902892500278
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 9221872812818225285}
- component: {fileID: 8317664859568879488}
- component: {fileID: 6478899211359510473}
m_Layer: 0
m_Name: BhopBoots_Pickup
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &9221872812818225285
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5967924902892500278}
serializedVersion: 2
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: 4275644691617312389}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &8317664859568879488
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5967924902892500278}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 95709fa8fd6c75045b1ef8e4f505c152, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::PickupItem
definition: {fileID: 11400000, guid: e09f7b05a155ba8449a0f0d8412ebe9d, type: 2}
itemName: Item
itemIcon: {fileID: 0}
spinSpeed: 140
bobHeight: 0.2
bobSpeed: 2.8
pickupRadius: 2.2
pickupKey: 101
pickupParticlesPrefab: {fileID: 0}
pickupSound: {fileID: 0}
--- !u!114 &6478899211359510473
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5967924902892500278}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 1ce88c8ee6fd8a0469215a541f72f835, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::StaminaBoostPickup
newMaxStamina: 200

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 9ead08e094f7bc74c86e58083dfcdf75
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ef5d1bcacc1d9e346b6e53fbcec62ad8
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,141 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &8985654986098955209
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 6346070617587720957}
- component: {fileID: 8585139455581857007}
- component: {fileID: 5387598106901245531}
m_Layer: 0
m_Name: GunSplat
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &6346070617587720957
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8985654986098955209}
serializedVersion: 2
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: 6241336930636794420}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &8585139455581857007
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8985654986098955209}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: e348b4e172ba23d45960c0071cc09b1f, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::SimpleGun
damage: 30
range: 120
fireRate: 6
maxAmmo: 24
currentAmmo: 0
isAutomatic: 0
recoilKickback: 0.08
recoilKickUp: 0.04
recoilRecoverySpeed: 12
enableInternalBob: 0
bobFrequency: 10
bobHorizontalAmplitude: 0.05
bobVerticalAmplitude: 0.03
sprintBobMultiplier: 1.5
bobReturnSpeed: 6
fpsCam: {fileID: 0}
--- !u!114 &5387598106901245531
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8985654986098955209}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 59433c687536a60418803799027ddcc3, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::WeaponViewmodel
positionOffset: {x: 0.13, y: -0.25, z: 0.45}
rotationOffset: {x: 0.03, y: 0, z: 0}
scale: {x: 1, y: 1, z: 1}
--- !u!1001 &8738524764220850214
PrefabInstance:
m_ObjectHideFlags: 0
serializedVersion: 2
m_Modification:
serializedVersion: 3
m_TransformParent: {fileID: 6346070617587720957}
m_Modifications:
- target: {fileID: -8098169881513260187, guid: 4f15ea45bfb5e4f11bc2bce50e39fc10, type: 3}
propertyPath: m_Name
value: Visual
objectReference: {fileID: 0}
- target: {fileID: 3447742079981357586, guid: 4f15ea45bfb5e4f11bc2bce50e39fc10, type: 3}
propertyPath: m_LocalPosition.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 3447742079981357586, guid: 4f15ea45bfb5e4f11bc2bce50e39fc10, type: 3}
propertyPath: m_LocalPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 3447742079981357586, guid: 4f15ea45bfb5e4f11bc2bce50e39fc10, type: 3}
propertyPath: m_LocalPosition.z
value: 0
objectReference: {fileID: 0}
- target: {fileID: 3447742079981357586, guid: 4f15ea45bfb5e4f11bc2bce50e39fc10, type: 3}
propertyPath: m_LocalRotation.w
value: 1
objectReference: {fileID: 0}
- target: {fileID: 3447742079981357586, guid: 4f15ea45bfb5e4f11bc2bce50e39fc10, type: 3}
propertyPath: m_LocalRotation.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 3447742079981357586, guid: 4f15ea45bfb5e4f11bc2bce50e39fc10, type: 3}
propertyPath: m_LocalRotation.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 3447742079981357586, guid: 4f15ea45bfb5e4f11bc2bce50e39fc10, type: 3}
propertyPath: m_LocalRotation.z
value: 0
objectReference: {fileID: 0}
- target: {fileID: 3447742079981357586, guid: 4f15ea45bfb5e4f11bc2bce50e39fc10, type: 3}
propertyPath: m_LocalEulerAnglesHint.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 3447742079981357586, guid: 4f15ea45bfb5e4f11bc2bce50e39fc10, type: 3}
propertyPath: m_LocalEulerAnglesHint.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 3447742079981357586, guid: 4f15ea45bfb5e4f11bc2bce50e39fc10, type: 3}
propertyPath: m_LocalEulerAnglesHint.z
value: 0
objectReference: {fileID: 0}
m_RemovedComponents: []
m_RemovedGameObjects: []
m_AddedGameObjects: []
m_AddedComponents: []
m_SourcePrefab: {fileID: 3150474306388093854, guid: 4f15ea45bfb5e4f11bc2bce50e39fc10, type: 3}
--- !u!4 &6241336930636794420 stripped
Transform:
m_CorrespondingSourceObject: {fileID: 3447742079981357586, guid: 4f15ea45bfb5e4f11bc2bce50e39fc10, type: 3}
m_PrefabInstance: {fileID: 8738524764220850214}
m_PrefabAsset: {fileID: 0}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: aa9cce2c129fa1a41bf5a539b136f7a6
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -340,9 +340,22 @@ PrefabInstance:
value: 0
objectReference: {fileID: 0}
m_RemovedComponents: []
m_RemovedGameObjects: []
m_RemovedGameObjects:
- {fileID: 2029899304289424255, guid: 33de4b621d70a49aab5df775f2b826ef, type: 3}
m_AddedGameObjects: []
m_AddedComponents: []
m_AddedComponents:
- targetCorrespondingSourceObject: {fileID: 6882903375964528458, guid: 33de4b621d70a49aab5df775f2b826ef, type: 3}
insertIndex: -1
addedObject: {fileID: 2039163533}
- targetCorrespondingSourceObject: {fileID: 6882903375964528458, guid: 33de4b621d70a49aab5df775f2b826ef, type: 3}
insertIndex: -1
addedObject: {fileID: 2039163534}
- targetCorrespondingSourceObject: {fileID: 6882903375964528458, guid: 33de4b621d70a49aab5df775f2b826ef, type: 3}
insertIndex: -1
addedObject: {fileID: 2039163535}
- targetCorrespondingSourceObject: {fileID: 6882903375964528458, guid: 33de4b621d70a49aab5df775f2b826ef, type: 3}
insertIndex: -1
addedObject: {fileID: 2039163536}
m_SourcePrefab: {fileID: 100100000, guid: 33de4b621d70a49aab5df775f2b826ef, type: 3}
--- !u!1001 &197010634
PrefabInstance:
@@ -6459,52 +6472,6 @@ MeshCollider:
m_Convex: 0
m_CookingOptions: 30
m_Mesh: {fileID: 0}
--- !u!1 &487503082
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 487503084}
- component: {fileID: 487503083}
m_Layer: 0
m_Name: GameObject (1)
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &487503083
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 487503082}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 180c6606991a4b64f812f9713907fd0c, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::EnemySpawner
enemyType: 2
healthOverride: 0
--- !u!4 &487503084
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 487503082}
serializedVersion: 2
m_LocalRotation: {x: 0.07095997, y: 0.5789249, z: -0.050682172, w: 0.8107047}
m_LocalPosition: {x: 243.82, y: -35.34, z: 91.07}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &571790233 stripped
GameObject:
m_CorrespondingSourceObject: {fileID: -8098169881513260187, guid: 3df8bb31de452e64fa85a1265898d698, type: 3}
@@ -6532,52 +6499,6 @@ MeshCollider:
m_Convex: 0
m_CookingOptions: 30
m_Mesh: {fileID: 1050908021570271116, guid: 3df8bb31de452e64fa85a1265898d698, type: 3}
--- !u!1 &606146705
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 606146707}
- component: {fileID: 606146706}
m_Layer: 0
m_Name: GameObject (2)
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &606146706
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 606146705}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 180c6606991a4b64f812f9713907fd0c, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::EnemySpawner
enemyType: 1
healthOverride: 0
--- !u!4 &606146707
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 606146705}
serializedVersion: 2
m_LocalRotation: {x: 0.07095997, y: 0.5789249, z: -0.050682172, w: 0.8107047}
m_LocalPosition: {x: 244.40219, y: -35.34, z: 89.64255}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &624019478 stripped
GameObject:
m_CorrespondingSourceObject: {fileID: -8220620780705863596, guid: 9e848a421785b4212abcb7cf4ae082f3, type: 3}
@@ -6732,6 +6653,33 @@ MonoBehaviour:
m_ShadowLayerMask: 1
m_RenderingLayers: 1
m_ShadowRenderingLayers: 1
--- !u!1 &715417260 stripped
GameObject:
m_CorrespondingSourceObject: {fileID: -8098169881513260187, guid: 681011e79237b4409ad76bc628bdf5b3, type: 3}
m_PrefabInstance: {fileID: 791320992}
m_PrefabAsset: {fileID: 0}
--- !u!114 &715417264
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 715417260}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 95709fa8fd6c75045b1ef8e4f505c152, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::PickupItem
definition: {fileID: 11400000, guid: e09f7b05a155ba8449a0f0d8412ebe9d, type: 2}
itemName: Item
itemIcon: {fileID: 0}
spinSpeed: 120
bobHeight: 0.18
bobSpeed: 2.2
pickupRadius: 2
pickupKey: 101
pickupParticlesPrefab: {fileID: 0}
pickupSound: {fileID: 0}
--- !u!1001 &791320992
PrefabInstance:
m_ObjectHideFlags: 0
@@ -6758,7 +6706,7 @@ PrefabInstance:
objectReference: {fileID: 0}
- target: {fileID: 3447742079981357586, guid: 681011e79237b4409ad76bc628bdf5b3, type: 3}
propertyPath: m_LocalPosition.x
value: 365.609
value: 363.888
objectReference: {fileID: 0}
- target: {fileID: 3447742079981357586, guid: 681011e79237b4409ad76bc628bdf5b3, type: 3}
propertyPath: m_LocalPosition.y
@@ -6766,7 +6714,7 @@ PrefabInstance:
objectReference: {fileID: 0}
- target: {fileID: 3447742079981357586, guid: 681011e79237b4409ad76bc628bdf5b3, type: 3}
propertyPath: m_LocalPosition.z
value: 156.609
value: 157.267
objectReference: {fileID: 0}
- target: {fileID: 3447742079981357586, guid: 681011e79237b4409ad76bc628bdf5b3, type: 3}
propertyPath: m_LocalRotation.w
@@ -6799,7 +6747,10 @@ PrefabInstance:
m_RemovedComponents: []
m_RemovedGameObjects: []
m_AddedGameObjects: []
m_AddedComponents: []
m_AddedComponents:
- targetCorrespondingSourceObject: {fileID: -8098169881513260187, guid: 681011e79237b4409ad76bc628bdf5b3, type: 3}
insertIndex: -1
addedObject: {fileID: 715417264}
m_SourcePrefab: {fileID: 3150474306388093854, guid: 681011e79237b4409ad76bc628bdf5b3, type: 3}
--- !u!1001 &806276174
PrefabInstance:
@@ -6919,6 +6870,66 @@ MeshCollider:
m_Convex: 0
m_CookingOptions: 30
m_Mesh: {fileID: 1050908021570271116, guid: 1e096925873fc492b854ee214231ab86, type: 3}
--- !u!1001 &1007367441
PrefabInstance:
m_ObjectHideFlags: 0
serializedVersion: 2
m_Modification:
serializedVersion: 3
m_TransformParent: {fileID: 0}
m_Modifications:
- target: {fileID: 6346070617587720957, guid: aa9cce2c129fa1a41bf5a539b136f7a6, type: 3}
propertyPath: m_LocalPosition.x
value: 355.01
objectReference: {fileID: 0}
- target: {fileID: 6346070617587720957, guid: aa9cce2c129fa1a41bf5a539b136f7a6, type: 3}
propertyPath: m_LocalPosition.y
value: -18.848
objectReference: {fileID: 0}
- target: {fileID: 6346070617587720957, guid: aa9cce2c129fa1a41bf5a539b136f7a6, type: 3}
propertyPath: m_LocalPosition.z
value: 167.46
objectReference: {fileID: 0}
- target: {fileID: 6346070617587720957, guid: aa9cce2c129fa1a41bf5a539b136f7a6, type: 3}
propertyPath: m_LocalRotation.w
value: 1
objectReference: {fileID: 0}
- target: {fileID: 6346070617587720957, guid: aa9cce2c129fa1a41bf5a539b136f7a6, type: 3}
propertyPath: m_LocalRotation.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 6346070617587720957, guid: aa9cce2c129fa1a41bf5a539b136f7a6, type: 3}
propertyPath: m_LocalRotation.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 6346070617587720957, guid: aa9cce2c129fa1a41bf5a539b136f7a6, type: 3}
propertyPath: m_LocalRotation.z
value: 0
objectReference: {fileID: 0}
- target: {fileID: 6346070617587720957, guid: aa9cce2c129fa1a41bf5a539b136f7a6, type: 3}
propertyPath: m_LocalEulerAnglesHint.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 6346070617587720957, guid: aa9cce2c129fa1a41bf5a539b136f7a6, type: 3}
propertyPath: m_LocalEulerAnglesHint.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 6346070617587720957, guid: aa9cce2c129fa1a41bf5a539b136f7a6, type: 3}
propertyPath: m_LocalEulerAnglesHint.z
value: 0
objectReference: {fileID: 0}
- target: {fileID: 8985654986098955209, guid: aa9cce2c129fa1a41bf5a539b136f7a6, type: 3}
propertyPath: m_Name
value: GunSplat
objectReference: {fileID: 0}
m_RemovedComponents: []
m_RemovedGameObjects: []
m_AddedGameObjects: []
m_AddedComponents:
- targetCorrespondingSourceObject: {fileID: 8985654986098955209, guid: aa9cce2c129fa1a41bf5a539b136f7a6, type: 3}
insertIndex: -1
addedObject: {fileID: 2128421432}
m_SourcePrefab: {fileID: 100100000, guid: aa9cce2c129fa1a41bf5a539b136f7a6, type: 3}
--- !u!1001 &1022368674
PrefabInstance:
m_ObjectHideFlags: 0
@@ -7125,6 +7136,60 @@ MeshCollider:
m_Convex: 0
m_CookingOptions: 30
m_Mesh: {fileID: 1050908021570271116, guid: d26387bf4a33f4f03b916907f6b50828, type: 3}
--- !u!1 &1114521747
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1114521749}
- component: {fileID: 1114521748}
m_Layer: 0
m_Name: gunsplat
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &1114521748
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1114521747}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 95709fa8fd6c75045b1ef8e4f505c152, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::PickupItem
definition: {fileID: 11400000, guid: 2b5a641a42945604c9ffa88754a9f6d1, type: 2}
itemName: Weapon
itemIcon: {fileID: 0}
spinSpeed: 120
bobHeight: 0.18
bobSpeed: 2.2
pickupRadius: 2
pickupKey: 101
pickupParticlesPrefab: {fileID: 0}
pickupSound: {fileID: 0}
--- !u!4 &1114521749
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1114521747}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 355.188, y: -16.728, z: 155.372}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &1121149997 stripped
GameObject:
m_CorrespondingSourceObject: {fileID: -8098169881513260187, guid: 3c8a5d44028bd40d3abe5e22cafcb901, type: 3}
@@ -7569,11 +7634,11 @@ PrefabInstance:
objectReference: {fileID: 0}
- target: {fileID: 3447742079981357586, guid: 4f15ea45bfb5e4f11bc2bce50e39fc10, type: 3}
propertyPath: m_LocalPosition.x
value: 304.18
value: -186547
objectReference: {fileID: 0}
- target: {fileID: 3447742079981357586, guid: 4f15ea45bfb5e4f11bc2bce50e39fc10, type: 3}
propertyPath: m_LocalPosition.y
value: -146.63
value: -74119
objectReference: {fileID: 0}
- target: {fileID: 3447742079981357586, guid: 4f15ea45bfb5e4f11bc2bce50e39fc10, type: 3}
propertyPath: m_LocalPosition.z
@@ -8155,52 +8220,6 @@ PrefabInstance:
insertIndex: -1
addedObject: {fileID: 277384082}
m_SourcePrefab: {fileID: 3150474306388093854, guid: 4e219defa86804242a4efe4306a4b26b, type: 3}
--- !u!1 &1882479419
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1882479421}
- component: {fileID: 1882479420}
m_Layer: 0
m_Name: GameObject (3)
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &1882479420
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1882479419}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 180c6606991a4b64f812f9713907fd0c, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::EnemySpawner
enemyType: 0
healthOverride: 0
--- !u!4 &1882479421
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1882479419}
serializedVersion: 2
m_LocalRotation: {x: 0.07095997, y: 0.5789249, z: -0.050682172, w: 0.8107047}
m_LocalPosition: {x: 243.82, y: -35.34, z: 91.07}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1001 &2026185005
PrefabInstance:
m_ObjectHideFlags: 0
@@ -8277,52 +8296,115 @@ PrefabInstance:
insertIndex: -1
addedObject: {fileID: 990201665}
m_SourcePrefab: {fileID: 3150474306388093854, guid: 1e096925873fc492b854ee214231ab86, type: 3}
--- !u!1 &2073272903
--- !u!1 &2039163524 stripped
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_CorrespondingSourceObject: {fileID: 6882903375964528458, guid: 33de4b621d70a49aab5df775f2b826ef, type: 3}
m_PrefabInstance: {fileID: 110786869}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 2073272905}
- component: {fileID: 2073272904}
m_Layer: 0
m_Name: GameObject
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &2073272904
--- !u!114 &2039163533
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2073272903}
m_GameObject: {fileID: 2039163524}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 180c6606991a4b64f812f9713907fd0c, type: 3}
m_Script: {fileID: 11500000, guid: 514ba78e0d4cf8b4787471c2db0c6976, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::EnemySpawner
enemyType: 1
healthOverride: 0
--- !u!4 &2073272905
Transform:
m_EditorClassIdentifier: Assembly-CSharp::Inventory
items: []
toggleKey: 105
equipKey: 102
columns: 4
maxSlots: 20
colBackground: {r: 0.05, g: 0.05, b: 0.05, a: 0.92}
colPanel: {r: 0.1, g: 0.1, b: 0.1, a: 0.95}
colSlotEmpty: {r: 0.18, g: 0.18, b: 0.18, a: 1}
colSlotFilled: {r: 0.22, g: 0.28, b: 0.22, a: 1}
colSlotHover: {r: 0.35, g: 0.45, b: 0.35, a: 1}
colSlotSelected: {r: 0.45, g: 0.75, b: 0.45, a: 1}
colSlotEquipped: {r: 0.6, g: 0.4, b: 0.1, a: 1}
colBorder: {r: 0.4, g: 0.65, b: 0.4, a: 1}
colText: {r: 0.85, g: 0.95, b: 0.85, a: 1}
colDim: {r: 0.5, g: 0.6, b: 0.5, a: 1}
colAccent: {r: 0.5, g: 0.9, b: 0.5, a: 1}
colWeaponAccent: {r: 0.95, g: 0.75, b: 0.2, a: 1}
colCtxBg: {r: 0.08, g: 0.1, b: 0.08, a: 0.98}
colCtxBorder: {r: 0.4, g: 0.65, b: 0.4, a: 1}
colCtxHover: {r: 0.2, g: 0.35, b: 0.2, a: 1}
colCtxText: {r: 0.85, g: 0.95, b: 0.85, a: 1}
colCtxDestructive: {r: 0.85, g: 0.25, b: 0.2, a: 1}
--- !u!114 &2039163534
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2073272903}
serializedVersion: 2
m_LocalRotation: {x: 0.07095997, y: 0.5789249, z: -0.050682172, w: 0.8107047}
m_LocalPosition: {x: 243.82, y: -35.34, z: 91.07}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_GameObject: {fileID: 2039163524}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 18715d388d5da7544b10c5d7a385cbee, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::WeaponManager
weaponHolder: {fileID: 0}
weaponPositionOffset: {x: 0.2, y: -0.25, z: 0.45}
weaponRotationOffset: {x: 0, y: 0, z: 0}
weaponScale: {x: 1, y: 1, z: 1}
allowScrollSwitch: 1
allowNumberKeys: 1
--- !u!114 &2039163535
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2039163524}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 1d84d3e6c1cf6794da6ff84203edb291, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::PlayerHUD
barWidth: 220
barHeight: 18
barSpacing: 10
edgePadX: 18
edgePadY: 18
anchor: 0
colHealthFull: {r: 0.15, g: 0.8, b: 0.25, a: 1}
colHealthMid: {r: 0.9, g: 0.75, b: 0.1, a: 1}
colHealthLow: {r: 0.9, g: 0.15, b: 0.1, a: 1}
healthMidThreshold: 0.5
healthLowThreshold: 0.25
colStaminaNormal: {r: 0.2, g: 0.55, b: 0.95, a: 1}
colStaminaRegen: {r: 0.2, g: 0.55, b: 0.95, a: 0.45}
colStaminaExhaust: {r: 0.6, g: 0.18, b: 0.18, a: 1}
colBarBackground: {r: 0.06, g: 0.06, b: 0.06, a: 0.9}
colBarBorder: {r: 0.28, g: 0.28, b: 0.28, a: 1}
colLabel: {r: 0.8, g: 0.8, b: 0.8, a: 1}
colLabelCritical: {r: 1, g: 0.25, b: 0.25, a: 1}
borderThickness: 1.5
pulseSpeed: 4
showSpeedometer: 1
colSpeedo: {r: 0.2, g: 0.95, b: 0.4, a: 1}
colSpeedoFast: {r: 0.95, g: 0.8, b: 0.1, a: 1}
colSpeedoCritical: {r: 0.95, g: 0.2, b: 0.2, a: 1}
speedoFastThreshold: 60
speedoCritThreshold: 75
--- !u!114 &2039163536
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2039163524}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 5680222822977cd4a988c93d7b8881bc, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::BootsEffect
bootsItemName: Bunny Hop Boots
boostedMaxStamina: 200
--- !u!1001 &2100225160
PrefabInstance:
m_ObjectHideFlags: 0
@@ -8445,6 +8527,33 @@ MeshCollider:
m_Convex: 0
m_CookingOptions: 30
m_Mesh: {fileID: 0}
--- !u!1 &2128421431 stripped
GameObject:
m_CorrespondingSourceObject: {fileID: 8985654986098955209, guid: aa9cce2c129fa1a41bf5a539b136f7a6, type: 3}
m_PrefabInstance: {fileID: 1007367441}
m_PrefabAsset: {fileID: 0}
--- !u!114 &2128421432
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2128421431}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 95709fa8fd6c75045b1ef8e4f505c152, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::PickupItem
definition: {fileID: 11400000, guid: 2b5a641a42945604c9ffa88754a9f6d1, type: 2}
itemName: Item
itemIcon: {fileID: 0}
spinSpeed: 120
bobHeight: 0.18
bobSpeed: 2.2
pickupRadius: 2
pickupKey: 101
pickupParticlesPrefab: {fileID: 0}
pickupSound: {fileID: 0}
--- !u!1660057539 &9223372036854775807
SceneRoots:
m_ObjectHideFlags: 0
@@ -8454,10 +8563,6 @@ SceneRoots:
- {fileID: 1719075356}
- {fileID: 1410308539}
- {fileID: 343869061}
- {fileID: 2073272905}
- {fileID: 487503084}
- {fileID: 606146707}
- {fileID: 1882479421}
- {fileID: 1360218911}
- {fileID: 271705868}
- {fileID: 1022368674}
@@ -8478,3 +8583,5 @@ SceneRoots:
- {fileID: 1716730849}
- {fileID: 2026185005}
- {fileID: 65142585}
- {fileID: 1114521749}
- {fileID: 1007367441}

View File

@@ -0,0 +1,46 @@
using UnityEngine;
/// <summary>
/// Attach to the Player. Watches the inventory for the Bunny Hop Boots being
/// equipped/unequipped and adjusts max stamina accordingly.
/// </summary>
public class BootsEffect : MonoBehaviour
{
[Tooltip("Must match the ItemDefinition itemName exactly.")]
public string bootsItemName = "Bunny Hop Boots";
public float boostedMaxStamina = 200f;
private Inventory _inventory;
private Player _player;
private bool _boosted = false;
void Start()
{
_inventory = GetComponent<Inventory>();
_player = GetComponent<Player>();
}
void Update()
{
if (_inventory == null || _player == null) return;
// Find the boots entry in inventory
var entry = _inventory.items.Find(e => e.DisplayName == bootsItemName);
bool shouldBoost = entry != null && entry.isEquipped;
if (shouldBoost && !_boosted)
{
_player.maxStamina = boostedMaxStamina;
_player.stamina = Mathf.Min(_player.stamina + (boostedMaxStamina - 100f), boostedMaxStamina);
_boosted = true;
Debug.Log("[BootsEffect] Boots equipped — stamina boosted to " + boostedMaxStamina);
}
else if (!shouldBoost && _boosted)
{
_player.maxStamina = 100f;
_player.stamina = Mathf.Min(_player.stamina, 100f);
_boosted = false;
Debug.Log("[BootsEffect] Boots unequipped — stamina restored to 100");
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5680222822977cd4a988c93d7b8881bc

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 384e9957bd5199549870be11d0838d80
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,100 @@
using UnityEngine;
using UnityEditor;
using System.IO;
/// <summary>
/// OGG → Setup → Create Bunny Hop Boots
/// Creates the ItemDefinition and a world pickup for the bhop boots.
/// </summary>
public static class BhopBootsSetup
{
private const string ITEM_DEF_PATH = "Assets/Items/BhopBoots_Item.asset";
private const string PICKUP_PREFAB = "Assets/Prefabs/Pickups/BhopBoots_Pickup.prefab";
[MenuItem("OGG/Setup/Create Bunny Hop Boots")]
public static void Create()
{
EnsureFolder("Assets/Items");
EnsureFolder("Assets/Prefabs/Pickups");
// ── ItemDefinition ────────────────────────────────────────────
ItemDefinition item = AssetDatabase.LoadAssetAtPath<ItemDefinition>(ITEM_DEF_PATH);
bool isNew = item == null;
if (isNew) item = ScriptableObject.CreateInstance<ItemDefinition>();
item.itemName = "Bunny Hop Boots";
item.type = ItemDefinition.ItemType.Misc;
item.isEquippable = true;
item.description = "Illegal footwear. Gives 200 stamina while worn. Do not wear near cliffs.";
if (isNew)
AssetDatabase.CreateAsset(item, ITEM_DEF_PATH);
else
EditorUtility.SetDirty(item);
AssetDatabase.SaveAssets();
// ── Pickup prefab — procedural boot shape ─────────────────────
GameObject root = new GameObject("BhopBoots_Pickup");
// Simple visual: a squashed cube (boot-ish)
GameObject body = GameObject.CreatePrimitive(PrimitiveType.Cube);
body.name = "Visual";
body.transform.SetParent(root.transform, false);
body.transform.localPosition = new Vector3(0f, 0f, 0f);
body.transform.localScale = new Vector3(0.4f, 0.2f, 0.7f);
Object.DestroyImmediate(body.GetComponent<Collider>());
// Bright accent colour so it stands out
var rend = body.GetComponent<Renderer>();
if (rend != null)
{
rend.material = new Material(Shader.Find("Universal Render Pipeline/Lit"));
rend.material.color = new Color(0.1f, 0.9f, 0.4f);
}
PickupItem pickup = root.AddComponent<PickupItem>();
pickup.definition = item;
pickup.spinSpeed = 140f;
pickup.bobHeight = 0.2f;
pickup.bobSpeed = 2.8f;
pickup.pickupRadius = 2.2f;
pickup.pickupKey = KeyCode.E;
StaminaBoostPickup boost = root.AddComponent<StaminaBoostPickup>();
boost.newMaxStamina = 200f;
if (File.Exists(Application.dataPath + "/../" + PICKUP_PREFAB))
AssetDatabase.DeleteAsset(PICKUP_PREFAB);
GameObject saved = PrefabUtility.SaveAsPrefabAsset(root, PICKUP_PREFAB);
Object.DestroyImmediate(root);
AssetDatabase.Refresh();
EditorUtility.DisplayDialog(
"Bunny Hop Boots ✓",
$"Created:\n• {ITEM_DEF_PATH}\n• {PICKUP_PREFAB}\n\n" +
"Drag BhopBoots_Pickup into the scene.\n" +
"Pick it up with [E] to unlock bunny hopping.\n\n" +
"Hold SPACE while landing to chain hops and build speed.\n" +
"Strafe left/right mid-air to steer.",
"Let's go");
EditorGUIUtility.PingObject(saved);
Selection.activeObject = saved;
}
static void EnsureFolder(string path)
{
string[] parts = path.Split('/');
string current = parts[0];
for (int i = 1; i < parts.Length; i++)
{
string next = current + "/" + parts[i];
if (!AssetDatabase.IsValidFolder(next))
AssetDatabase.CreateFolder(current, parts[i]);
current = next;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 92ff81f98e809934bab9c5b8f160276a

View File

@@ -0,0 +1,151 @@
using UnityEngine;
using UnityEditor;
using System.IO;
/// <summary>
/// OGG → Setup → Create Gun Splat Weapon
/// Builds:
/// • Assets/Prefabs/Weapons/GunSplat.prefab — the held weapon (SimpleGun)
/// • Assets/Prefabs/Pickups/GunSplat_Pickup.prefab — world pickup (spins/bobs, press E)
/// • Assets/Items/GunSplat_Item.asset — ItemDefinition (type = Weapon)
/// </summary>
public static class GunSplatSetup
{
private const string GLB_PATH = "Assets/Models/LidarScans/Gun Splat.glb";
private const string WEAPON_PREFAB = "Assets/Prefabs/Weapons/GunSplat.prefab";
private const string PICKUP_PREFAB = "Assets/Prefabs/Pickups/GunSplat_Pickup.prefab";
private const string ITEM_DEF_PATH = "Assets/Items/GunSplat_Item.asset";
[MenuItem("OGG/Setup/Create Gun Splat Weapon")]
public static void CreateGunSplat()
{
// ── 1. Ensure output folders exist ──────────────────────────
EnsureFolder("Assets/Prefabs/Weapons");
EnsureFolder("Assets/Prefabs/Pickups");
EnsureFolder("Assets/Items");
// ── 2. Load the GLB model ────────────────────────────────────
GameObject modelAsset = AssetDatabase.LoadAssetAtPath<GameObject>(GLB_PATH);
if (modelAsset == null)
{
Debug.LogError($"[GunSplatSetup] Could not find GLB at: {GLB_PATH}");
EditorUtility.DisplayDialog("Gun Splat Setup", $"Could not find model at:\n{GLB_PATH}", "OK");
return;
}
// ── 3. Build the held-weapon prefab ──────────────────────────
GameObject weaponRoot = new GameObject("GunSplat");
AttachVisual(weaponRoot, modelAsset);
SimpleGun gun = weaponRoot.AddComponent<SimpleGun>();
gun.damage = 30f;
gun.range = 120f;
gun.fireRate = 6f;
gun.maxAmmo = 24;
gun.isAutomatic = false;
if (File.Exists(DataRelative(WEAPON_PREFAB)))
AssetDatabase.DeleteAsset(WEAPON_PREFAB);
GameObject savedWeapon = PrefabUtility.SaveAsPrefabAsset(weaponRoot, WEAPON_PREFAB);
Object.DestroyImmediate(weaponRoot);
if (savedWeapon == null)
{
Debug.LogError("[GunSplatSetup] Failed to save weapon prefab.");
return;
}
Debug.Log($"[GunSplatSetup] Weapon prefab → {WEAPON_PREFAB}");
// ── 4. Create / update ItemDefinition ────────────────────────
ItemDefinition item = AssetDatabase.LoadAssetAtPath<ItemDefinition>(ITEM_DEF_PATH);
bool isNew = item == null;
if (isNew) item = ScriptableObject.CreateInstance<ItemDefinition>();
item.itemName = "Gun Splat";
item.type = ItemDefinition.ItemType.Weapon;
item.weaponPrefab = savedWeapon;
item.description = "A janky lidar-scanned sidearm. Shoots first, looks weird always.";
if (isNew)
AssetDatabase.CreateAsset(item, ITEM_DEF_PATH);
else
EditorUtility.SetDirty(item);
AssetDatabase.SaveAssets();
Debug.Log($"[GunSplatSetup] ItemDefinition → {ITEM_DEF_PATH}");
// ── 5. Build the world pickup prefab ─────────────────────────
GameObject pickupRoot = new GameObject("GunSplat_Pickup");
// Visual child — the GLB model
AttachVisual(pickupRoot, modelAsset);
// PickupItem component
PickupItem pickup = pickupRoot.AddComponent<PickupItem>();
pickup.definition = item;
pickup.spinSpeed = 90f;
pickup.bobHeight = 0.15f;
pickup.bobSpeed = 2.0f;
pickup.pickupRadius = 2.2f;
pickup.pickupKey = KeyCode.E;
if (File.Exists(DataRelative(PICKUP_PREFAB)))
AssetDatabase.DeleteAsset(PICKUP_PREFAB);
GameObject savedPickup = PrefabUtility.SaveAsPrefabAsset(pickupRoot, PICKUP_PREFAB);
Object.DestroyImmediate(pickupRoot);
if (savedPickup == null)
{
Debug.LogError("[GunSplatSetup] Failed to save pickup prefab.");
return;
}
Debug.Log($"[GunSplatSetup] Pickup prefab → {PICKUP_PREFAB}");
AssetDatabase.Refresh();
// ── 6. Done ──────────────────────────────────────────────────
EditorUtility.DisplayDialog(
"Gun Splat Setup ✓",
"Created:\n" +
$"• {WEAPON_PREFAB}\n" +
$"• {PICKUP_PREFAB}\n" +
$"• {ITEM_DEF_PATH}\n\n" +
"Drag GunSplat_Pickup into the scene wherever you want it to spawn.\n" +
"It will spin, bob, and show an [E] prompt when the player gets close.",
"Sick");
EditorGUIUtility.PingObject(savedPickup);
Selection.activeObject = savedPickup;
}
// ─── Helpers ─────────────────────────────────────────────────────
static void AttachVisual(GameObject parent, GameObject modelAsset)
{
GameObject visual = (GameObject)PrefabUtility.InstantiatePrefab(modelAsset);
if (visual == null) visual = Object.Instantiate(modelAsset);
visual.name = "Visual";
visual.transform.SetParent(parent.transform, false);
visual.transform.localPosition = Vector3.zero;
visual.transform.localRotation = Quaternion.identity;
visual.transform.localScale = Vector3.one;
}
static string DataRelative(string assetPath) =>
Application.dataPath + "/../" + assetPath;
static void EnsureFolder(string path)
{
string[] parts = path.Split('/');
string current = parts[0];
for (int i = 1; i < parts.Length; i++)
{
string next = current + "/" + parts[i];
if (!AssetDatabase.IsValidFolder(next))
AssetDatabase.CreateFolder(current, parts[i]);
current = next;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: a0ceea0a64e3fa344b83004be51fe457

View File

@@ -0,0 +1,92 @@
using UnityEngine;
using UnityEditor;
[CustomEditor(typeof(WeaponManager))]
public class WeaponManagerEditor : Editor
{
public override void OnInspectorGUI()
{
DrawDefaultInspector();
WeaponManager wm = (WeaponManager)target;
if (!Application.isPlaying) return;
EditorGUILayout.Space();
EditorGUILayout.LabelField("── Live Positioning ──", EditorStyles.boldLabel);
// Show what's active
string activeName = wm.ActiveWeaponName;
if (string.IsNullOrEmpty(activeName))
{
EditorGUILayout.HelpBox("No weapon currently equipped.", MessageType.Info);
return;
}
EditorGUILayout.HelpBox($"Active: {activeName}", MessageType.None);
// Select the live GO so you can drag gizmos in Scene view
if (GUILayout.Button("🎯 Select Active Weapon in Scene"))
{
var slot = wm.slots.Find(s => s.itemName == activeName);
if (slot != null)
{
Selection.activeGameObject = slot.instance;
SceneView.lastActiveSceneView?.Focus();
}
}
EditorGUILayout.Space();
// Sync the current transform back into the WeaponViewmodel (or fallback global offset)
if (GUILayout.Button("⬆ Sync Transform → Offset Fields"))
{
var slot = wm.slots.Find(s => s.itemName == activeName);
if (slot != null)
{
var vm = slot.instance.GetComponent<WeaponViewmodel>();
if (vm != null)
{
Undo.RecordObject(vm, "Sync Weapon Viewmodel");
vm.SyncFromTransform();
EditorUtility.SetDirty(vm);
// Also save back to the source prefab asset
var prefabAsset = PrefabUtility.GetCorrespondingObjectFromSource(slot.instance);
if (prefabAsset != null)
{
var prefabVm = prefabAsset.GetComponent<WeaponViewmodel>();
if (prefabVm != null)
{
Undo.RecordObject(prefabVm, "Sync Weapon Viewmodel Prefab");
prefabVm.positionOffset = vm.positionOffset;
prefabVm.rotationOffset = vm.rotationOffset;
prefabVm.scale = vm.scale;
EditorUtility.SetDirty(prefabVm);
AssetDatabase.SaveAssets();
}
}
Debug.Log($"[WeaponManager] Synced to WeaponViewmodel: pos={vm.positionOffset} rot={vm.rotationOffset} scale={vm.scale}");
}
else
{
// Fallback: no viewmodel, write to global offsets
Undo.RecordObject(wm, "Sync Weapon Offset");
wm.weaponPositionOffset = slot.instance.transform.localPosition;
wm.weaponRotationOffset = slot.instance.transform.localRotation.eulerAngles;
wm.weaponScale = slot.instance.transform.localScale;
EditorUtility.SetDirty(wm);
Debug.Log($"[WeaponManager] Synced to global offset: pos={wm.weaponPositionOffset}");
}
}
}
EditorGUILayout.HelpBox(
"1. Hit Play & equip the gun\n" +
"2. Click 'Select Active Weapon'\n" +
"3. Use Move/Rotate gizmos in Scene view\n" +
"4. Click 'Sync Transform → Offset Fields'\n" +
"5. Stop Play — values are saved",
MessageType.Info);
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: fe67de8a32d4bf5449ee8def34a1bb8a

View File

@@ -4,33 +4,31 @@ using UnityEngine;
public class FirstPersonController : MonoBehaviour
{
[Header("Movement Settings")]
public float walkSpeed = 50f; // Boosted from 8f to overcome collision issues
public float runSpeed = 80f; // Boosted from 14f
public float walkSpeed = 50f;
public float runSpeed = 80f;
public float jumpHeight = 2.5f;
public float gravity = -20f;
public float gravity = -20f;
[Header("Mouse Look Settings")]
public float mouseSensitivity = 3f;
public float maxLookAngle = 90f;
public float maxLookAngle = 90f;
[Header("References")]
public Camera playerCamera;
// Private variables
private CharacterController controller;
private Vector3 velocity;
private bool isGrounded;
private float xRotation = 0f;
private Inventory inventory;
private Player player;
private Vector3 velocity;
private bool isGrounded;
private float xRotation = 0f;
void Start()
{
Debug.Log("Starting game");
// FORCE NORMAL TIME (in case something external changed it)
Time.timeScale = 1f;
controller = GetComponent<CharacterController>();
if (controller == null)
{
Debug.LogError("FirstPersonController: No CharacterController found!");
@@ -40,85 +38,58 @@ public class FirstPersonController : MonoBehaviour
if (playerCamera == null)
playerCamera = GetComponentInChildren<Camera>();
inventory = GetComponent<Inventory>();
player = GetComponent<Player>();
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
Cursor.visible = false;
}
void Update()
{
if( controller == null )
return;
if (controller == null) return;
isGrounded = controller.isGrounded;
if (isGrounded && velocity.y < 0)
velocity.y = -2f;
// ─── Movement via direct KeyCode ───
float moveX = 0f;
float moveZ = 0f;
if (Input.GetKey(KeyCode.W) || Input.GetKey(KeyCode.UpArrow)) moveZ += 1f;
if (Input.GetKey(KeyCode.S) || Input.GetKey(KeyCode.DownArrow)) moveZ -= 1f;
if (Input.GetKey(KeyCode.A) || Input.GetKey(KeyCode.LeftArrow)) moveX -= 1f;
if (Input.GetKey(KeyCode.D) || Input.GetKey(KeyCode.RightArrow)) moveX += 1f;
// DEBUG: Log once when W is first pressed
if (Input.GetKeyDown(KeyCode.W))
{
Debug.Log($"[FPC DEBUG] W pressed | controller.enabled={controller.enabled} | position={transform.position} | isGrounded={isGrounded}");
Debug.Log($"[FPC DEBUG] Time.timeScale={Time.timeScale} | Time.deltaTime={Time.deltaTime} | walkSpeed={walkSpeed}");
}
float moveX = 0f, moveZ = 0f;
if (Input.GetKey(KeyCode.W) || Input.GetKey(KeyCode.UpArrow)) moveZ += 1f;
if (Input.GetKey(KeyCode.S) || Input.GetKey(KeyCode.DownArrow)) moveZ -= 1f;
if (Input.GetKey(KeyCode.A) || Input.GetKey(KeyCode.LeftArrow)) moveX -= 1f;
if (Input.GetKey(KeyCode.D) || Input.GetKey(KeyCode.RightArrow)) moveX += 1f;
Vector3 move = transform.right * moveX + transform.forward * moveZ;
if (move.magnitude > 1f) move.Normalize();
if (move.magnitude > 1f)
move.Normalize();
bool wantSprint = Input.GetKey(KeyCode.LeftShift) && move.magnitude > 0f;
bool isSprinting = wantSprint && (player == null || player.CanSprint());
if (player != null) player.isSprinting = isSprinting;
float currentSpeed = Input.GetKey(KeyCode.LeftShift) ? runSpeed : walkSpeed;
Vector3 posBefore = transform.position;
float currentSpeed = isSprinting ? runSpeed : walkSpeed;
controller.Move(move * currentSpeed * Time.deltaTime);
// DEBUG: Log if we tried to move but didn't
if (move.magnitude > 0f && Input.GetKeyDown(KeyCode.W))
{
Vector3 posAfter = transform.position;
//Debug.Log($"[FPC DEBUG] Move attempt: delta={move * currentSpeed * Time.deltaTime} | actualDelta={(posAfter - posBefore)} | controller.height={controller.height} | controller.radius={controller.radius}");
// Check what we're colliding with
Collider[] nearbyColliders = Physics.OverlapSphere(transform.position, controller.radius + 0.5f);
//Debug.Log($"[FPC DEBUG] Found {nearbyColliders.Length} colliders near player");
foreach (Collider col in nearbyColliders)
{
if (col != controller && !(col is CharacterController))
Debug.LogWarning($"[FPC DEBUG] Nearby collider: {col.gameObject.name} on layer {LayerMask.LayerToName(col.gameObject.layer)}");
}
}
// Jumping
if (Input.GetKey(KeyCode.Space) && isGrounded)
velocity.y = Mathf.Sqrt(jumpHeight * -2f * gravity);
// Gravity
velocity.y += gravity * Time.deltaTime;
controller.Move(velocity * Time.deltaTime);
controller.Move(new Vector3(0f, velocity.y, 0f) * Time.deltaTime);
// Mouse look
HandleMouseLook();
bool inventoryOpen = inventory != null && inventory.IsOpen;
if (!inventoryOpen)
HandleMouseLook();
// Escape to unlock cursor
if (Input.GetKeyDown(KeyCode.Escape))
{
Cursor.lockState = CursorLockMode.None;
Cursor.visible = true;
Cursor.visible = true;
}
// Click to re-lock cursor
if (Input.GetMouseButtonDown(0) && Cursor.lockState == CursorLockMode.None)
if (Input.GetMouseButtonDown(0) && Cursor.lockState == CursorLockMode.None && !inventoryOpen)
{
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
Cursor.visible = false;
}
}
@@ -128,9 +99,8 @@ public class FirstPersonController : MonoBehaviour
float mouseY = Input.GetAxis("Mouse Y") * mouseSensitivity;
xRotation -= mouseY;
xRotation = Mathf.Clamp(xRotation, -maxLookAngle, maxLookAngle);
xRotation = Mathf.Clamp(xRotation, -maxLookAngle, maxLookAngle);
playerCamera.transform.localRotation = Quaternion.Euler(xRotation, 0f, 0f);
transform.Rotate(Vector3.up * mouseX);
}
}

View File

@@ -45,7 +45,7 @@ public static class InputData
new MouseButton( Defines.Input.kLeftMouseButton),
new KeyboardButton( KeyCode.LeftControl ),
} ) );
reload = AddButton( new KeyboardButton( KeyCode.Space ) );
reload = AddButton( new KeyboardButton( KeyCode.R ) );
jump = AddButton( new KeyboardButton( KeyCode.Space ) );
horizontalMovement = AddAxis( new MultiAxis(

View File

@@ -187,8 +187,13 @@ public class HumanoidEnemy : MonoBehaviour
// Punch animation
StartCoroutine(PunchAnimation());
// TODO: Hook into player health system when you have one
// player.GetComponent<PlayerHealth>()?.TakeDamage(attackDamage);
// Damage the player via Player.health
Player playerHealth = player.GetComponent<Player>();
if (playerHealth != null)
{
playerHealth.health -= attackDamage;
Debug.Log($"[Enemy] Player health: {playerHealth.health}");
}
}
}

632
Assets/Scripts/Inventory.cs Normal file
View File

@@ -0,0 +1,632 @@
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// Inventory system with a toggleable HUD (press I) and right-click context menu.
/// Attach to the Player GameObject alongside WeaponManager.
/// </summary>
public class Inventory : MonoBehaviour
{
// ─── Item data ────────────────────────────────────────────────────
[System.Serializable]
public class InventoryEntry
{
public ItemDefinition definition;
public string itemName;
public Sprite icon;
public int count;
public bool isEquipped;
public InventoryEntry(string name, Sprite icon)
{
this.itemName = name;
this.icon = icon;
this.count = 1;
this.isEquipped = false;
}
public InventoryEntry(ItemDefinition def)
{
this.definition = def;
this.itemName = def.itemName;
this.icon = def.icon;
this.count = 1;
this.isEquipped = false;
}
public bool _isWeaponSlot;
public bool IsWeapon =>
(definition != null && definition.type == ItemDefinition.ItemType.Weapon)
|| _isWeaponSlot;
// True for weapons AND equippable misc items
public bool IsEquippable =>
IsWeapon || (definition != null && definition.isEquippable);
public string DisplayName => definition != null ? definition.itemName : itemName;
}
public List<InventoryEntry> items = new List<InventoryEntry>();
// ─── HUD config ───────────────────────────────────────────────────
[Header("HUD")]
public KeyCode toggleKey = KeyCode.I;
public KeyCode equipKey = KeyCode.F;
public int columns = 4;
public int maxSlots = 20;
[Header("HUD Colours")]
public Color colBackground = new Color(0.05f, 0.05f, 0.05f, 0.92f);
public Color colPanel = new Color(0.10f, 0.10f, 0.10f, 0.95f);
public Color colSlotEmpty = new Color(0.18f, 0.18f, 0.18f, 1.00f);
public Color colSlotFilled = new Color(0.22f, 0.28f, 0.22f, 1.00f);
public Color colSlotHover = new Color(0.35f, 0.45f, 0.35f, 1.00f);
public Color colSlotSelected = new Color(0.45f, 0.75f, 0.45f, 1.00f);
public Color colSlotEquipped = new Color(0.60f, 0.40f, 0.10f, 1.00f);
public Color colBorder = new Color(0.40f, 0.65f, 0.40f, 1.00f);
public Color colText = new Color(0.85f, 0.95f, 0.85f, 1.00f);
public Color colDim = new Color(0.50f, 0.60f, 0.50f, 1.00f);
public Color colAccent = new Color(0.50f, 0.90f, 0.50f, 1.00f);
public Color colWeaponAccent = new Color(0.95f, 0.75f, 0.20f, 1.00f);
[Header("Context Menu Colours")]
public Color colCtxBg = new Color(0.08f, 0.10f, 0.08f, 0.98f);
public Color colCtxBorder = new Color(0.40f, 0.65f, 0.40f, 1.00f);
public Color colCtxHover = new Color(0.20f, 0.35f, 0.20f, 1.00f);
public Color colCtxText = new Color(0.85f, 0.95f, 0.85f, 1.00f);
public Color colCtxDestructive = new Color(0.85f, 0.25f, 0.20f, 1.00f);
// ─── Private state ────────────────────────────────────────────────
private bool _open = false;
private int _selectedIndex = 0;
private int _hoveredIndex = -1;
private WeaponManager _weaponManager;
// ─── Context menu state ───────────────────────────────────────────
private bool _ctxOpen = false;
private int _ctxItemIndex = -1;
private Vector2 _ctxPos;
private const float kCtxItemH = 28f;
private const float kCtxWidth = 140f;
private const float kCtxPadX = 10f;
// Context menu action labels — built per item
private struct CtxAction
{
public string label;
public bool isDestructive;
public System.Action callback;
}
private List<CtxAction> _ctxActions = new List<CtxAction>();
// ─── Layout constants ─────────────────────────────────────────────
private const float kSlotSize = 80f;
private const float kSlotPad = 8f;
private const float kPanelPad = 20f;
private const float kHeaderH = 40f;
private const float kDetailPanelH = 120f;
// ─────────────────────────────────────────────────────────────────
void Start()
{
_weaponManager = GetComponent<WeaponManager>();
if (_weaponManager != null)
Invoke(nameof(SyncStartingWeapons), 0.1f);
}
void SyncStartingWeapons()
{
if (_weaponManager == null) return;
foreach (var slot in _weaponManager.slots)
{
if (slot.definition != null)
AddItem(slot.definition, autoEquip: false);
else
AddItemRaw(slot.itemName, null, isWeaponSlot: true);
}
SyncEquippedState();
}
// ─── Public API ───────────────────────────────────────────────────
public void AddItem(ItemDefinition def, bool autoEquip = false)
{
if (def == null) return;
InventoryEntry existing = items.Find(e => e.definition == def);
if (existing != null)
{
if (def.type != ItemDefinition.ItemType.Weapon) existing.count++;
Debug.Log($"[Inventory] Already have: {def.itemName}");
}
else
{
var entry = new InventoryEntry(def);
items.Add(entry);
Debug.Log($"[Inventory] Picked up: {def.itemName}");
if (def.type == ItemDefinition.ItemType.Weapon)
{
_weaponManager?.AddWeapon(def);
if (autoEquip) EquipItem(items.Count - 1);
}
}
}
public void AddItem(string itemName, Sprite icon = null) => AddItemRaw(itemName, icon, false);
private void AddItemRaw(string itemName, Sprite icon, bool isWeaponSlot)
{
InventoryEntry existing = items.Find(e => e.itemName == itemName && e.definition == null);
if (existing != null) { existing.count++; return; }
var entry = new InventoryEntry(itemName, icon);
entry._isWeaponSlot = isWeaponSlot;
items.Add(entry);
}
public bool RemoveItem(string itemName, int amount = 1)
{
InventoryEntry entry = items.Find(e => e.DisplayName == itemName);
if (entry == null) return false;
entry.count -= amount;
if (entry.count <= 0) items.Remove(entry);
return true;
}
public bool HasItem(string itemName) => items.Exists(e => e.DisplayName == itemName);
public int CountOf(string itemName)
{
var e = items.Find(x => x.DisplayName == itemName);
return e != null ? e.count : 0;
}
public bool IsOpen => _open;
// ─── Equip / Unequip ──────────────────────────────────────────────
void EquipItem(int index)
{
if (index < 0 || index >= items.Count) return;
InventoryEntry entry = items[index];
if (!entry.IsEquippable) return;
if (entry.IsWeapon)
{
// Unequip other weapons
foreach (var e in items) { if (e.IsWeapon) e.isEquipped = false; }
entry.isEquipped = true;
_weaponManager?.EquipByName(entry.DisplayName);
}
else
{
// Toggle equip for misc equippables (boots, etc.)
entry.isEquipped = true;
}
Debug.Log($"[Inventory] Equipped: {entry.DisplayName}");
}
void UnequipItem(int index)
{
if (index < 0 || index >= items.Count) return;
InventoryEntry entry = items[index];
if (!entry.IsEquippable || !entry.isEquipped) return;
entry.isEquipped = false;
if (entry.IsWeapon && _weaponManager != null)
{
var slot = _weaponManager.slots.Find(s => s.itemName == entry.DisplayName);
if (slot != null) slot.instance.SetActive(false);
_weaponManager.Unequip();
}
Debug.Log($"[Inventory] Unequipped: {entry.DisplayName}");
}
void DropItem(int index)
{
if (index < 0 || index >= items.Count) return;
InventoryEntry entry = items[index];
if (entry.isEquipped) UnequipItem(index);
items.RemoveAt(index);
if (_selectedIndex >= items.Count) _selectedIndex = items.Count - 1;
Debug.Log($"[Inventory] Dropped: {entry.DisplayName}");
// TODO: optionally spawn the pickup back in the world here
}
void SyncEquippedState()
{
if (_weaponManager == null) return;
string active = _weaponManager.ActiveWeaponName;
foreach (var e in items)
{
// Only sync weapon equipped state — leave misc equippables alone
if (e.IsWeapon)
e.isEquipped = (e.DisplayName == active);
}
}
// ─── Context menu builder ─────────────────────────────────────────
void OpenContextMenu(int itemIndex, Vector2 screenPos)
{
if (itemIndex < 0 || itemIndex >= items.Count) return;
_ctxItemIndex = itemIndex;
_ctxActions.Clear();
InventoryEntry entry = items[itemIndex];
if (entry.IsEquippable)
{
if (entry.isEquipped)
{
int captured = itemIndex;
_ctxActions.Add(new CtxAction
{
label = "Unequip",
isDestructive = false,
callback = () => UnequipItem(captured)
});
}
else
{
int captured = itemIndex;
_ctxActions.Add(new CtxAction
{
label = "Equip",
isDestructive = false,
callback = () => EquipItem(captured)
});
}
}
// Consumables / misc could have a "Use" action here in future
// For now everyone gets Inspect (just selects and focuses the detail panel)
{
int captured = itemIndex;
_ctxActions.Add(new CtxAction
{
label = "Inspect",
isDestructive = false,
callback = () => _selectedIndex = captured
});
}
{
int captured = itemIndex;
_ctxActions.Add(new CtxAction
{
label = "Drop",
isDestructive = true,
callback = () => DropItem(captured)
});
}
// Clamp so the menu doesn't go off screen
float menuH = _ctxActions.Count * kCtxItemH + 8f;
float clampedX = Mathf.Min(screenPos.x, Screen.width - kCtxWidth - 4f);
float clampedY = Mathf.Min(screenPos.y, Screen.height - menuH - 4f);
_ctxPos = new Vector2(clampedX, clampedY);
_ctxOpen = true;
}
void CloseContextMenu() { _ctxOpen = false; _ctxItemIndex = -1; }
// ─── Toggle ───────────────────────────────────────────────────────
void Update()
{
if (Input.GetKeyDown(toggleKey))
{
CloseContextMenu();
SetOpen(!_open);
}
if (_open && Input.GetKeyDown(equipKey))
{
if (_selectedIndex < items.Count && items[_selectedIndex].IsWeapon)
EquipItem(_selectedIndex);
CloseContextMenu();
}
if (!_open)
SyncEquippedState();
}
void SetOpen(bool open)
{
_open = open;
SyncEquippedState();
Cursor.lockState = open ? CursorLockMode.None : CursorLockMode.Locked;
Cursor.visible = open;
}
// ─── GUI ──────────────────────────────────────────────────────────
void OnGUI()
{
if (!_open) return;
int rows = Mathf.CeilToInt((float)maxSlots / columns);
float gridW = columns * (kSlotSize + kSlotPad) - kSlotPad;
float gridH = rows * (kSlotSize + kSlotPad) - kSlotPad;
float panelW = gridW + kPanelPad * 2f;
float panelH = kHeaderH + kPanelPad + gridH + kPanelPad + kDetailPanelH + kPanelPad;
float panelX = (Screen.width - panelW) * 0.5f;
float panelY = (Screen.height - panelH) * 0.5f;
Vector2 mouse = Event.current.mousePosition;
// ── Click outside context menu to close it ────────────────────
if (_ctxOpen && Event.current.type == EventType.MouseDown)
{
Rect ctxRect = GetCtxRect();
if (!ctxRect.Contains(mouse))
CloseContextMenu();
}
// ── Backdrop ──────────────────────────────────────────────────
DrawRect(new Rect(0, 0, Screen.width, Screen.height), new Color(0, 0, 0, 0.55f));
// ── Main panel ────────────────────────────────────────────────
DrawBorderedBox(new Rect(panelX, panelY, panelW, panelH), colPanel, colBorder, 2f);
GUIStyle headerStyle = MakeStyle(18, FontStyle.Bold, colAccent, TextAnchor.MiddleCenter);
GUI.Label(new Rect(panelX, panelY + 8f, panelW, 28f), "[ INVENTORY ]", headerStyle);
DrawRect(new Rect(panelX + 10f, panelY + kHeaderH - 2f, panelW - 20f, 1f), colBorder);
if (_weaponManager != null && _weaponManager.ActiveWeaponName != "")
{
GUIStyle activeStyle = MakeStyle(11, FontStyle.Normal, colWeaponAccent, TextAnchor.MiddleRight);
GUI.Label(new Rect(panelX, panelY + 10f, panelW - 10f, 20f),
$"EQUIPPED: {_weaponManager.ActiveWeaponName}", activeStyle);
}
float gx = panelX + kPanelPad;
float gy = panelY + kHeaderH + kPanelPad;
_hoveredIndex = -1;
// ── Slot grid ─────────────────────────────────────────────────
for (int i = 0; i < maxSlots; i++)
{
int col = i % columns;
int row = i / columns;
float sx = gx + col * (kSlotSize + kSlotPad);
float sy = gy + row * (kSlotSize + kSlotPad);
Rect slotRect = new Rect(sx, sy, kSlotSize, kSlotSize);
bool hasItem = i < items.Count;
bool isHover = slotRect.Contains(mouse) && !_ctxOpen;
bool isSel = (i == _selectedIndex);
bool isEquipped = hasItem && items[i].isEquipped;
bool isWeapon = hasItem && items[i].IsWeapon;
if (isHover) _hoveredIndex = i;
// Left-click: select
if (isHover && Event.current.type == EventType.MouseDown
&& Event.current.button == 0 && hasItem)
{
_selectedIndex = i;
CloseContextMenu();
}
// Left double-click: equip weapon
if (isHover && Event.current.type == EventType.MouseDown
&& Event.current.button == 0 && Event.current.clickCount == 2 && isWeapon)
EquipItem(i);
// Right-click: open context menu
if (isHover && Event.current.type == EventType.MouseDown
&& Event.current.button == 1 && hasItem)
{
_selectedIndex = i;
OpenContextMenu(i, mouse);
}
// Slot colour
Color slotCol;
if (isEquipped) slotCol = colSlotEquipped;
else if (isSel) slotCol = colSlotSelected;
else if (isHover) slotCol = colSlotHover;
else if (hasItem) slotCol = colSlotFilled;
else slotCol = colSlotEmpty;
Color borderCol = isEquipped ? colWeaponAccent : (isSel ? colAccent : colBorder);
DrawBorderedBox(slotRect, slotCol, borderCol, (isSel || isEquipped) ? 2f : 1f);
if (!hasItem) continue;
InventoryEntry entry = items[i];
// Icon or letter placeholder
if (entry.icon != null)
{
float ip = 10f;
GUI.DrawTexture(new Rect(sx + ip, sy + ip, kSlotSize - ip * 2f, kSlotSize - 28f),
entry.icon.texture, ScaleMode.ScaleToFit, true);
}
else
{
Color lc = isEquipped ? colWeaponAccent : (isSel ? Color.white : colAccent);
GUI.Label(new Rect(sx, sy + 8f, kSlotSize, kSlotSize - 24f),
entry.DisplayName.Substring(0, 1).ToUpper(), MakeStyle(26, FontStyle.Bold, lc, TextAnchor.MiddleCenter));
}
// Weapon tag
if (isWeapon)
{
Color tc = isEquipped ? colWeaponAccent : new Color(0.6f, 0.5f, 0.2f, 1f);
GUI.Label(new Rect(sx + 3f, sy + 2f, 40f, 12f),
isEquipped ? "EQUIP" : "WPN", MakeStyle(8, FontStyle.Bold, tc, TextAnchor.UpperLeft));
}
// Right-click hint on hovered slot
if (isHover)
{
GUIStyle hint = MakeStyle(8, FontStyle.Normal, new Color(0.6f, 0.6f, 0.6f, 0.9f), TextAnchor.LowerRight);
GUI.Label(new Rect(sx + 2f, sy + 2f, kSlotSize - 4f, kSlotSize - 4f), "RMB ▾", hint);
}
// Item name
string dn = entry.DisplayName.Length > 10 ? entry.DisplayName.Substring(0, 9) + "…" : entry.DisplayName;
Color nc = isEquipped ? colWeaponAccent : (isSel ? Color.white : colText);
GUI.Label(new Rect(sx + 2f, sy + kSlotSize - 28f, kSlotSize - 4f, 16f),
dn, MakeStyle(9, FontStyle.Bold, nc, TextAnchor.LowerCenter));
// Count badge
if (entry.count > 1)
{
DrawRect(new Rect(sx + kSlotSize - 22f, sy + kSlotSize - 18f, 20f, 16f), new Color(0, 0, 0, 0.7f));
GUI.Label(new Rect(sx + kSlotSize - 22f, sy + kSlotSize - 18f, 19f, 16f),
$"x{entry.count}", MakeStyle(10, FontStyle.Bold, Color.white, TextAnchor.LowerRight));
}
}
// ── Detail panel ──────────────────────────────────────────────
float detailY = gy + gridH + kPanelPad;
Rect detailRect = new Rect(panelX + kPanelPad, detailY, panelW - kPanelPad * 2f, kDetailPanelH - kPanelPad);
DrawBorderedBox(detailRect, new Color(0.08f, 0.08f, 0.08f, 1f), colBorder, 1f);
if (_selectedIndex < items.Count)
{
InventoryEntry sel = items[_selectedIndex];
float dy = detailRect.y + 8f;
float dx = detailRect.x + 10f;
float dw = detailRect.width - 20f;
GUI.Label(new Rect(dx, dy, dw, 16f), "SELECTED", MakeStyle(10, FontStyle.Normal, colDim, TextAnchor.UpperLeft));
GUI.Label(new Rect(dx, dy + 16f, dw, 22f), sel.DisplayName, MakeStyle(14, FontStyle.Bold, colText, TextAnchor.UpperLeft));
string desc = (sel.definition != null && sel.definition.description != "")
? sel.definition.description : (sel.IsWeapon ? "Weapon" : "Item");
GUI.Label(new Rect(dx, dy + 38f, dw * 0.65f, 30f), desc, MakeStyle(10, FontStyle.Normal, colDim, TextAnchor.UpperLeft));
if (sel.IsEquippable)
{
Color wc = sel.isEquipped ? colWeaponAccent : colDim;
string tag = sel.isEquipped ? "▶ EQUIPPED" : "right-click or [F] to equip";
GUI.Label(new Rect(dx, dy + 68f, dw, 18f), tag, MakeStyle(11, FontStyle.Bold, wc, TextAnchor.UpperLeft));
}
else if (sel.count > 1)
{
GUI.Label(new Rect(dx, dy + 68f, dw, 18f), $"Quantity: {sel.count}",
MakeStyle(11, FontStyle.Bold, colAccent, TextAnchor.UpperLeft));
}
// Equip / Unequip button bottom-right of detail panel
if (sel.IsEquippable)
{
Rect btnRect = new Rect(detailRect.x + detailRect.width - 110f,
detailRect.y + detailRect.height - 34f, 100f, 26f);
if (sel.isEquipped)
{
DrawBorderedBox(btnRect, new Color(0.4f, 0.15f, 0.05f, 1f), colWeaponAccent, 1f);
GUI.Label(btnRect, "[ F ] UNEQUIP", MakeStyle(11, FontStyle.Bold, Color.white, TextAnchor.MiddleCenter));
if (GUI.Button(btnRect, "", GUIStyle.none)) UnequipItem(_selectedIndex);
}
else
{
DrawBorderedBox(btnRect, colSlotEquipped, colWeaponAccent, 1f);
GUI.Label(btnRect, "[ F ] EQUIP", MakeStyle(11, FontStyle.Bold, Color.white, TextAnchor.MiddleCenter));
if (GUI.Button(btnRect, "", GUIStyle.none)) EquipItem(_selectedIndex);
}
}
}
else
{
GUI.Label(detailRect, "No item selected", MakeStyle(11, FontStyle.Normal, colDim, TextAnchor.MiddleCenter));
}
// Hints
GUIStyle hintStyle = MakeStyle(10, FontStyle.Normal, colDim, TextAnchor.LowerRight);
GUI.Label(new Rect(panelX, panelY + panelH - 18f, panelW - 8f, 16f), "[ I ] close | RMB slot for options", hintStyle);
// ── Context menu (drawn last so it's on top) ──────────────────
if (_ctxOpen)
DrawContextMenu(mouse);
}
// ─── Context menu draw ────────────────────────────────────────────
Rect GetCtxRect()
{
float menuH = _ctxActions.Count * kCtxItemH + 8f;
return new Rect(_ctxPos.x, _ctxPos.y, kCtxWidth, menuH);
}
void DrawContextMenu(Vector2 mouse)
{
float menuH = _ctxActions.Count * kCtxItemH + 8f;
Rect bgRect = new Rect(_ctxPos.x, _ctxPos.y, kCtxWidth, menuH);
// Shadow
DrawRect(new Rect(bgRect.x + 3f, bgRect.y + 3f, bgRect.width, bgRect.height),
new Color(0f, 0f, 0f, 0.45f));
// Background + border
DrawBorderedBox(bgRect, colCtxBg, colCtxBorder, 1.5f);
// Header strip — item name
if (_ctxItemIndex >= 0 && _ctxItemIndex < items.Count)
{
string title = items[_ctxItemIndex].DisplayName;
DrawRect(new Rect(bgRect.x + 1.5f, bgRect.y + 1.5f, bgRect.width - 3f, 20f),
new Color(0.15f, 0.22f, 0.15f, 1f));
GUI.Label(new Rect(bgRect.x + kCtxPadX, bgRect.y + 3f, kCtxWidth - kCtxPadX * 2f, 16f),
title.ToUpper(), MakeStyle(9, FontStyle.Bold, colWeaponAccent, TextAnchor.MiddleLeft));
}
// Action rows
for (int i = 0; i < _ctxActions.Count; i++)
{
float rowY = bgRect.y + 22f + i * kCtxItemH;
Rect rowRect = new Rect(bgRect.x + 1.5f, rowY, bgRect.width - 3f, kCtxItemH);
bool hover = rowRect.Contains(mouse);
if (hover)
DrawRect(rowRect, colCtxHover);
Color textColor = _ctxActions[i].isDestructive
? (hover ? Color.white : colCtxDestructive)
: (hover ? Color.white : colCtxText);
GUI.Label(new Rect(bgRect.x + kCtxPadX, rowY + 2f, kCtxWidth - kCtxPadX * 2f, kCtxItemH - 4f),
_ctxActions[i].label, MakeStyle(12, FontStyle.Normal, textColor, TextAnchor.MiddleLeft));
// Separator line between items
if (i < _ctxActions.Count - 1)
DrawRect(new Rect(bgRect.x + 6f, rowY + kCtxItemH - 1f, bgRect.width - 12f, 1f),
new Color(1f, 1f, 1f, 0.06f));
// Click to fire action
if (hover && Event.current.type == EventType.MouseDown && Event.current.button == 0)
{
_ctxActions[i].callback?.Invoke();
CloseContextMenu();
Event.current.Use();
}
}
}
// ─── Style / Draw helpers ─────────────────────────────────────────
static GUIStyle MakeStyle(int size, FontStyle style, Color color, TextAnchor align)
{
var s = new GUIStyle();
s.fontSize = size;
s.fontStyle = style;
s.normal.textColor = color;
s.alignment = align;
return s;
}
static void DrawRect(Rect r, Color c)
{
Color prev = GUI.color;
GUI.color = c;
GUI.DrawTexture(r, Texture2D.whiteTexture);
GUI.color = prev;
}
static void DrawBorderedBox(Rect r, Color fill, Color border, float thickness)
{
DrawRect(r, border);
DrawRect(new Rect(r.x + thickness, r.y + thickness,
r.width - thickness * 2f,
r.height - thickness * 2f), fill);
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 514ba78e0d4cf8b4787471c2db0c6976

View File

@@ -0,0 +1,28 @@
using UnityEngine;
/// <summary>
/// Create via right-click → Create → OGG → Item Definition
/// Drag onto PickupItem components in the scene.
/// </summary>
[CreateAssetMenu(fileName = "New Item", menuName = "OGG/Item Definition")]
public class ItemDefinition : ScriptableObject
{
public enum ItemType { Misc, Consumable, Weapon }
[Header("Identity")]
public string itemName = "Unnamed Item";
public Sprite icon;
public ItemType type = ItemType.Misc;
[Header("Weapon (only if type == Weapon)")]
[Tooltip("The prefab that gets spawned in the weapon holder when this is equipped.")]
public GameObject weaponPrefab;
[Header("Equipment")]
[Tooltip("If true, this item can be equipped/unequipped from the inventory even if it isn't a weapon.")]
public bool isEquippable = false;
[Header("Flavour")]
[TextArea(2, 4)]
public string description = "";
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b549159f64e1b164082e2e4ae4d28ac1

View File

@@ -0,0 +1,156 @@
using UnityEngine;
/// <summary>
/// Attach to any pickup object. Spins + bobs like MGS rations.
/// Assign an ItemDefinition asset for full weapon/equip support,
/// or just fill itemName for a simple non-weapon pickup.
/// </summary>
public class PickupItem : MonoBehaviour
{
[Header("Item")]
[Tooltip("Preferred: drag an ItemDefinition ScriptableObject here.")]
public ItemDefinition definition;
[Tooltip("Fallback name if no ItemDefinition is set.")]
public string itemName = "Item";
public Sprite itemIcon;
[Header("Spin & Bob")]
public float spinSpeed = 120f;
public float bobHeight = 0.18f;
public float bobSpeed = 2.2f;
[Header("Pickup")]
public float pickupRadius = 2.0f;
public KeyCode pickupKey = KeyCode.E;
[Header("FX")]
public GameObject pickupParticlesPrefab;
public AudioClip pickupSound;
// ─────────────────────────────────────────
private Vector3 _startPos;
private Transform _playerTransform;
private Inventory _inventory;
private AudioSource _audioSource;
private bool _collected = false;
// Prompt display
private bool _inRange = false;
void Start()
{
_startPos = transform.position;
Player player = FindObjectOfType<Player>();
if (player != null)
{
_playerTransform = player.transform;
_inventory = player.GetComponent<Inventory>();
}
_audioSource = gameObject.AddComponent<AudioSource>();
_audioSource.spatialBlend = 1f;
_audioSource.playOnAwake = false;
// If we have a definition, use its name/icon
if (definition != null)
{
itemName = definition.itemName;
itemIcon = definition.icon;
}
}
void Update()
{
if (_collected) return;
// ── Spin ──────────────────────────────
transform.Rotate(Vector3.up, spinSpeed * Time.deltaTime, Space.World);
// ── Bob ───────────────────────────────
float newY = _startPos.y + Mathf.Sin(Time.time * bobSpeed * Mathf.PI * 2f) * bobHeight;
transform.position = new Vector3(transform.position.x, newY, transform.position.z);
// ── Pickup check ─────────────────────
if (_playerTransform == null) return;
float dist = Vector3.Distance(transform.position, _playerTransform.position);
_inRange = dist <= pickupRadius;
if (_inRange && Input.GetKeyDown(pickupKey))
Collect();
}
void OnGUI()
{
if (_collected || !_inRange) return;
// World-to-screen prompt
Vector3 screenPos = Camera.main != null
? Camera.main.WorldToScreenPoint(_startPos + Vector3.up * (bobHeight + 0.4f))
: Vector3.zero;
if (screenPos.z < 0) return; // behind camera
float sx = screenPos.x;
float sy = Screen.height - screenPos.y; // flip y
string label = definition != null ? definition.itemName : itemName;
bool isWeapon = definition != null && definition.type == ItemDefinition.ItemType.Weapon;
string prompt = $"[{pickupKey}] Pick up {label}";
GUIStyle bg = new GUIStyle(GUI.skin.box);
bg.fontSize = 12;
bg.fontStyle = FontStyle.Bold;
bg.normal.textColor = isWeapon ? new Color(1f, 0.85f, 0.2f) : Color.white;
Vector2 size = bg.CalcSize(new GUIContent(prompt));
Rect bgRect = new Rect(sx - size.x * 0.5f - 6f, sy - size.y - 4f, size.x + 12f, size.y + 6f);
// Dark backing box
Color prev = GUI.color;
GUI.color = new Color(0, 0, 0, 0.65f);
GUI.DrawTexture(bgRect, Texture2D.whiteTexture);
GUI.color = prev;
GUI.Label(bgRect, " " + prompt + " ", bg);
}
void OnDrawGizmosSelected()
{
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(transform.position, pickupRadius);
}
void Collect()
{
_collected = true;
_inRange = false;
if (_inventory != null)
{
if (definition != null)
_inventory.AddItem(definition, autoEquip: true);
else
_inventory.AddItem(itemName, itemIcon);
}
else
{
Debug.LogWarning("PickupItem: No Inventory found on player!");
}
if (pickupParticlesPrefab != null)
Instantiate(pickupParticlesPrefab, transform.position, Quaternion.identity);
if (pickupSound != null)
{
_audioSource.PlayOneShot(pickupSound);
Destroy(gameObject, pickupSound.length);
}
else
{
Destroy(gameObject);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 95709fa8fd6c75045b1ef8e4f505c152

View File

@@ -1,6 +1,65 @@
using UnityEngine;
/// <summary>
/// Core player stats: health and stamina.
/// Stamina drains while sprinting, regens when not.
/// If stamina hits zero the player is exhausted and can't sprint again until it fully refills.
/// </summary>
public class Player : MonoBehaviour
{
public float health;
[Header("Health")]
public float maxHealth = 100f;
public float health = 100f;
[Header("Stamina")]
public float maxStamina = 100f;
public float stamina = 100f;
public float staminaDrain = 22f; // per second while sprinting
public float staminaRegen = 12f; // per second while not sprinting
public float regenDelay = 1.2f; // seconds after stopping sprint before regen kicks in
// ─── State ───────────────────────────────────────────────────────
[HideInInspector] public bool isSprinting = false;
[HideInInspector] public bool isExhausted = false; // true until stamina fully refills
private float _regenTimer = 0f;
// ─────────────────────────────────────────────────────────────────
void Update()
{
if (isSprinting && !isExhausted)
{
stamina -= staminaDrain * Time.deltaTime;
_regenTimer = 0f;
if (stamina <= 0f)
{
stamina = 0f;
isExhausted = true;
}
}
else
{
// Count down regen delay
_regenTimer += Time.deltaTime;
if (_regenTimer >= regenDelay)
{
stamina = Mathf.MoveTowards(stamina, maxStamina, staminaRegen * Time.deltaTime);
// Clear exhaustion only once fully refilled
if (isExhausted && stamina >= maxStamina)
isExhausted = false;
}
}
health = Mathf.Clamp(health, 0f, maxHealth);
stamina = Mathf.Clamp(stamina, 0f, maxStamina);
}
// ─── Helpers other scripts can call ──────────────────────────────
public bool CanSprint() => !isExhausted && stamina > 0f;
public float HealthFraction => health / maxHealth;
public float StaminaFraction => stamina / maxStamina;
}

281
Assets/Scripts/PlayerHUD.cs Normal file
View File

@@ -0,0 +1,281 @@
using UnityEngine;
/// <summary>
/// Draws the player health and stamina bars.
/// Attach to the Player GameObject alongside Player.cs
///
/// Deliberately ugly. This is a Cruelty Squad game.
/// </summary>
public class PlayerHUD : MonoBehaviour
{
[Header("Layout")]
public float barWidth = 220f;
public float barHeight = 16f;
public float barSpacing = 10f; // gap between health and stamina bars
public float edgePadX = 18f;
public float edgePadY = 18f;
public AnchorCorner anchor = AnchorCorner.BottomLeft;
public enum AnchorCorner { BottomLeft, BottomRight, TopLeft, TopRight }
[Header("Health Bar")]
public Color colHealthFull = new Color(0.15f, 0.80f, 0.25f, 1f);
public Color colHealthMid = new Color(0.85f, 0.75f, 0.05f, 1f);
public Color colHealthLow = new Color(0.90f, 0.12f, 0.12f, 1f);
public float healthMidThreshold = 0.5f;
public float healthLowThreshold = 0.25f;
[Header("Stamina Bar")]
public Color colStaminaNormal = new Color(0.20f, 0.55f, 0.95f, 1f);
public Color colStaminaRegen = new Color(0.20f, 0.55f, 0.95f, 0.45f); // dimmed while regenerating
public Color colStaminaExhaust = new Color(0.60f, 0.18f, 0.18f, 1f); // red flash when tapped out
[Header("Shared Style")]
public Color colBarBackground = new Color(0.06f, 0.06f, 0.06f, 0.90f);
public Color colBarBorder = new Color(0.28f, 0.28f, 0.28f, 1.00f);
public Color colLabel = new Color(0.80f, 0.80f, 0.80f, 1.00f);
public Color colLabelCritical = new Color(1.00f, 0.25f, 0.25f, 1.00f);
public float borderThickness = 1.5f;
[Header("Pulse")]
[Tooltip("The low-health bar pulses opacity when below the low threshold.")]
public float pulseSpeed = 3.5f;
[Header("Speedometer")]
public bool showSpeedometer = true;
public Color colSpeedo = new Color(0.20f, 0.95f, 0.40f, 1f);
public Color colSpeedoFast = new Color(0.95f, 0.80f, 0.10f, 1f);
public Color colSpeedoCritical = new Color(0.95f, 0.20f, 0.20f, 1f);
public float speedoFastThreshold = 60f;
public float speedoCritThreshold = 75f;
// ─── Private ─────────────────────────────────────────────────────
private Player _player;
private CharacterController _cc;
private Texture2D _white;
// Smooth display values so bars glide rather than snap
private float _displayHealth = 1f;
private float _displayStamina = 1f;
private const float kSmoothSpeed = 8f;
// Speed tracking
private Vector3 _lastPos;
private float _displaySpeed;
// ─────────────────────────────────────────────────────────────────
void Start()
{
_player = GetComponent<Player>();
_cc = GetComponent<CharacterController>();
_white = Texture2D.whiteTexture;
_lastPos = transform.position;
if (_player == null)
Debug.LogWarning("[PlayerHUD] No Player component found on this GameObject!");
}
void Update()
{
if (_player == null) return;
// Smooth bar fill values towards actual values
_displayHealth = Mathf.Lerp(_displayHealth, _player.HealthFraction, Time.deltaTime * kSmoothSpeed);
_displayStamina = Mathf.Lerp(_displayStamina, _player.StaminaFraction, Time.deltaTime * kSmoothSpeed);
// Calculate speed from position delta (horizontal only)
Vector3 pos = transform.position;
float rawSpeed = new Vector3(pos.x - _lastPos.x, 0f, pos.z - _lastPos.z).magnitude / Time.deltaTime;
_displaySpeed = Mathf.Lerp(_displaySpeed, rawSpeed, Time.deltaTime * 10f);
_lastPos = pos;
}
void OnGUI()
{
if (_player == null) return;
float totalH = barHeight * 2f + barSpacing + 20f; // label heights included
float totalW = barWidth;
float ox, oy;
switch (anchor)
{
case AnchorCorner.BottomLeft:
ox = edgePadX;
oy = Screen.height - totalH - edgePadY;
break;
case AnchorCorner.BottomRight:
ox = Screen.width - totalW - edgePadX;
oy = Screen.height - totalH - edgePadY;
break;
case AnchorCorner.TopLeft:
ox = edgePadX;
oy = edgePadY;
break;
default: // TopRight
ox = Screen.width - totalW - edgePadX;
oy = edgePadY;
break;
}
float cursor = oy;
// ── Health bar ───────────────────────────────────────────────
cursor = DrawStatBar(
ox, cursor,
label: "HEALTH",
value: _player.health,
maxValue: _player.maxHealth,
displayFraction: _displayHealth,
fillColor: GetHealthColor(),
pulse: _player.HealthFraction <= healthLowThreshold,
labelCritical: _player.HealthFraction <= healthLowThreshold
);
cursor += barSpacing;
// ── Stamina bar ──────────────────────────────────────────────
Color staminaColor;
if (_player.isExhausted)
staminaColor = colStaminaExhaust;
else if (!_player.isSprinting && _player.StaminaFraction < 1f)
staminaColor = colStaminaRegen;
else
staminaColor = colStaminaNormal;
string staminaLabel = _player.isExhausted ? "STAMINA [EXHAUSTED]" : "STAMINA";
DrawStatBar(
ox, cursor,
label: staminaLabel,
value: _player.stamina,
maxValue: _player.maxStamina,
displayFraction: _displayStamina,
fillColor: staminaColor,
pulse: _player.isExhausted,
labelCritical: _player.isExhausted
);
// ── Speedometer ──────────────────────────────────────────────
if (showSpeedometer && _cc != null)
DrawSpeedometer(ox, oy - 90f);
}
void DrawSpeedometer(float x, float y)
{
float speed = _displaySpeed;
Color col;
if (speed >= speedoCritThreshold) col = colSpeedoCritical;
else if (speed >= speedoFastThreshold) col = colSpeedoFast;
else col = colSpeedo;
float w = 130f;
float h = 70f;
// Dark backing box
DrawTex(new Rect(x - 6f, y - 6f, w + 12f, h + 12f), new Color(0f, 0f, 0f, 0.55f));
// Coloured left edge accent
DrawTex(new Rect(x - 6f, y - 6f, 3f, h + 12f), col);
// "SPEED" label
GUIStyle labelStyle = new GUIStyle();
labelStyle.fontSize = 10;
labelStyle.fontStyle = FontStyle.Bold;
labelStyle.normal.textColor = new Color(col.r, col.g, col.b, 0.75f);
GUI.Label(new Rect(x, y, w, 16f), "SPEED", labelStyle);
// Big number
GUIStyle numStyle = new GUIStyle();
numStyle.fontSize = 36;
numStyle.fontStyle = FontStyle.Bold;
numStyle.normal.textColor = col;
GUI.Label(new Rect(x, y + 14f, w, 42f), $"{speed:F1}", numStyle);
// Unit
GUIStyle unitStyle = new GUIStyle();
unitStyle.fontSize = 10;
unitStyle.fontStyle = FontStyle.Normal;
unitStyle.normal.textColor = new Color(col.r, col.g, col.b, 0.55f);
GUI.Label(new Rect(x, y + 54f, w, 16f), "units / sec", unitStyle);
}
// ─── Bar renderer ─────────────────────────────────────────────────
/// <summary>Draws one labelled bar. Returns the Y position after the bar.</summary>
float DrawStatBar(
float x, float y,
string label,
float value, float maxValue,
float displayFraction,
Color fillColor,
bool pulse,
bool labelCritical)
{
float labelH = 14f;
float innerW = barWidth - borderThickness * 2f;
float innerH = barHeight - borderThickness * 2f;
// ── Label ────────────────────────────────────────────────────
GUIStyle labelStyle = new GUIStyle();
labelStyle.fontSize = 10;
labelStyle.fontStyle = FontStyle.Bold;
labelStyle.normal.textColor = labelCritical ? colLabelCritical : colLabel;
string valueStr = $"{Mathf.CeilToInt(value)} / {Mathf.CeilToInt(maxValue)}";
GUI.Label(new Rect(x, y, barWidth * 0.6f, labelH), label, labelStyle);
GUIStyle valStyle = new GUIStyle(labelStyle);
valStyle.alignment = TextAnchor.UpperRight;
valStyle.normal.textColor = labelCritical ? colLabelCritical : colLabel;
GUI.Label(new Rect(x, y, barWidth, labelH), valueStr, valStyle);
float barY = y + labelH + 2f;
// ── Background ───────────────────────────────────────────────
DrawTex(new Rect(x, barY, barWidth, barHeight), colBarBorder);
DrawTex(new Rect(x + borderThickness, barY + borderThickness, innerW, innerH), colBarBackground);
// ── Fill ─────────────────────────────────────────────────────
float fillW = Mathf.Max(0f, displayFraction * innerW);
// Pulse alpha on low health / exhaustion
Color drawColor = fillColor;
if (pulse)
{
float alpha = Mathf.Lerp(0.35f, 1f, (Mathf.Sin(Time.time * pulseSpeed) + 1f) * 0.5f);
drawColor = new Color(fillColor.r, fillColor.g, fillColor.b, fillColor.a * alpha);
}
if (fillW > 0f)
DrawTex(new Rect(x + borderThickness, barY + borderThickness, fillW, innerH), drawColor);
// ── Segment tick marks (every 25%) ───────────────────────────
for (int i = 1; i < 4; i++)
{
float tickX = x + borderThickness + innerW * (i / 4f);
DrawTex(new Rect(tickX - 0.5f, barY + borderThickness, 1f, innerH),
new Color(0f, 0f, 0f, 0.45f));
}
return barY + barHeight; // bottom of bar
}
// ─── Helpers ─────────────────────────────────────────────────────
Color GetHealthColor()
{
float f = _player.HealthFraction;
if (f <= healthLowThreshold)
return colHealthLow;
if (f <= healthMidThreshold)
return Color.Lerp(colHealthLow, colHealthMid, (f - healthLowThreshold) / (healthMidThreshold - healthLowThreshold));
return Color.Lerp(colHealthMid, colHealthFull, (f - healthMidThreshold) / (1f - healthMidThreshold));
}
void DrawTex(Rect r, Color c)
{
Color prev = GUI.color;
GUI.color = c;
GUI.DrawTexture(r, _white);
GUI.color = prev;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1d84d3e6c1cf6794da6ff84203edb291

View File

@@ -16,6 +16,8 @@ public class SimpleGun : MonoBehaviour
public float recoilRecoverySpeed = 12f;
[Header("Weapon Bob Settings")]
[Tooltip("Set false if WeaponBob component is on the Camera — they'll double-stack otherwise.")]
public bool enableInternalBob = false;
public float bobFrequency = 10f;
public float bobHorizontalAmplitude = 0.05f;
public float bobVerticalAmplitude = 0.03f;
@@ -48,6 +50,10 @@ public class SimpleGun : MonoBehaviour
// Fire timing
private float nextTimeToFire = 0f;
// Cached refs for inventory check
private Inventory _inventory;
private WeaponManager _weaponManager;
void Start()
{
currentAmmo = maxAmmo;
@@ -58,7 +64,12 @@ public class SimpleGun : MonoBehaviour
fpsCam = GetComponentInParent<Camera>();
playerController = GetComponentInParent<CharacterController>();
originalLocalPos = transform.localPosition;
_inventory = GetComponentInParent<Inventory>();
_weaponManager = GetComponentInParent<WeaponManager>();
// originalLocalPos is driven by WeaponManager's offset — don't cache here
originalLocalPos = _weaponManager != null
? _weaponManager.weaponPositionOffset
: transform.localPosition;
// NUCLEAR OPTION: Remove ANY collider on this object or children
@@ -93,6 +104,10 @@ public class SimpleGun : MonoBehaviour
void Update()
{
// Don't shoot while the inventory is open
bool inventoryOpen = _inventory != null && _inventory.IsOpen;
if (inventoryOpen) return;
// Shooting
if (isAutomatic ? InputData.leftMouseButton.down : InputData.leftMouseButton.pressed)
{
@@ -107,11 +122,18 @@ public class SimpleGun : MonoBehaviour
if (InputData.reload.pressed)
Reload();
// Keep base position in sync with WeaponManager's live offset
if (_weaponManager != null)
originalLocalPos = _weaponManager.weaponPositionOffset;
// Recover from recoil
recoilOffset = Vector3.Lerp(recoilOffset, Vector3.zero, Time.deltaTime * recoilRecoverySpeed);
// Weapon bob
UpdateBob();
// Weapon bob (only if internal bob is enabled — disable if WeaponBob is on the Camera)
if (enableInternalBob)
UpdateBob();
else
bobOffset = Vector3.zero;
// Apply combined position: original + recoil + bob
transform.localPosition = originalLocalPos + recoilOffset + bobOffset;

View File

@@ -0,0 +1,36 @@
using UnityEngine;
/// <summary>
/// Add to a pickup GameObject alongside PickupItem.
/// When the item is collected, applies a one-time boost to the player's max stamina.
/// </summary>
[RequireComponent(typeof(PickupItem))]
public class StaminaBoostPickup : MonoBehaviour
{
[Tooltip("Max stamina will be set to this value when picked up.")]
public float newMaxStamina = 200f;
void Start()
{
// Hook into PickupItem's collect event via a simple poll
var pickup = GetComponent<PickupItem>();
// We override by watching PickupItem's collected state each frame
StartCoroutine(WatchForCollect(pickup));
}
System.Collections.IEnumerator WatchForCollect(PickupItem pickup)
{
// Wait until the pickup is destroyed (collected)
while (pickup != null)
yield return null;
// Find player and apply boost
Player p = FindObjectOfType<Player>();
if (p != null)
{
p.maxStamina = newMaxStamina;
p.stamina = newMaxStamina;
Debug.Log($"[StaminaBoost] Max stamina set to {newMaxStamina}");
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1ce88c8ee6fd8a0469215a541f72f835

View File

@@ -0,0 +1,197 @@
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// Attach to the Player GameObject.
/// Manages all instantiated weapon instances and switching between them.
/// Also registers any weapon already in the scene (e.g. the starting gun).
/// </summary>
public class WeaponManager : MonoBehaviour
{
[Header("References")]
[Tooltip("The transform weapons are parented to — usually the Camera or a WeaponHolder child.")]
public Transform weaponHolder;
[Header("Weapon Position")]
[Tooltip("Local position offset applied to all equipped weapons.")]
public Vector3 weaponPositionOffset = new Vector3(0.2f, -0.25f, 0.45f);
[Tooltip("Local euler rotation offset applied to all equipped weapons.")]
public Vector3 weaponRotationOffset = new Vector3(0f, 0f, 0f);
[Tooltip("Scale applied to all equipped weapons.")]
public Vector3 weaponScale = new Vector3(1f, 1f, 1f);
[Header("Switching")]
public bool allowScrollSwitch = true;
public bool allowNumberKeys = true;
// ─── internal slot ───────────────────────────────────────────────
public class WeaponSlot
{
public string itemName;
public GameObject instance; // the live GO in weaponHolder
public ItemDefinition definition; // may be null for the starting gun
}
public List<WeaponSlot> slots = new List<WeaponSlot>();
public int activeIndex { get; private set; } = -1;
// ─────────────────────────────────────────────────────────────────
void Start()
{
// Auto-find weapon holder if not set
if (weaponHolder == null)
{
Camera cam = GetComponentInChildren<Camera>();
weaponHolder = cam != null ? cam.transform : transform;
}
// Register any SimpleGun already present as children of the holder
SimpleGun[] existing = weaponHolder.GetComponentsInChildren<SimpleGun>(true);
foreach (SimpleGun gun in existing)
{
RegisterExistingGun(gun.gameObject);
}
}
private Inventory _inventory;
void Update()
{
if (slots.Count == 0) return;
// Live-update active weapon transform so Inspector tweaks take effect immediately
if (activeIndex >= 0 && activeIndex < slots.Count)
{
var vm = slots[activeIndex].instance.GetComponent<WeaponViewmodel>();
if (vm != null)
vm.Apply();
else
{
Transform t = slots[activeIndex].instance.transform;
t.localPosition = weaponPositionOffset;
t.localRotation = Quaternion.Euler(weaponRotationOffset);
t.localScale = weaponScale;
}
}
// Don't switch weapons while the inventory UI is open
if (_inventory == null) _inventory = GetComponent<Inventory>();
if (_inventory != null && _inventory.IsOpen) return;
// Scroll wheel
if (allowScrollSwitch)
{
float scroll = Input.GetAxis("Mouse ScrollWheel");
if (scroll > 0f) CycleWeapon(1);
if (scroll < 0f) CycleWeapon(-1);
}
// Number keys 19
if (allowNumberKeys)
{
for (int i = 0; i < Mathf.Min(slots.Count, 9); i++)
{
if (Input.GetKeyDown(KeyCode.Alpha1 + i))
EquipByIndex(i);
}
}
}
// ─── Public API ──────────────────────────────────────────────────
/// <summary>Register a weapon that already exists in the scene (starting gun).</summary>
public void RegisterExistingGun(GameObject gunGO, ItemDefinition def = null)
{
string name = def != null ? def.itemName : gunGO.name;
// Don't double-register
if (slots.Exists(s => s.instance == gunGO)) return;
slots.Add(new WeaponSlot { itemName = name, instance = gunGO, definition = def });
int idx = slots.Count - 1;
// Always start inactive — player must pick up / equip from inventory
gunGO.SetActive(false);
Debug.Log($"[WeaponManager] Registered existing weapon: {name} at slot {idx} (inactive)");
}
/// <summary>Add a new weapon from a definition (called by Inventory on first equip).</summary>
public int AddWeapon(ItemDefinition def)
{
if (def == null || def.weaponPrefab == null)
{
Debug.LogWarning("[WeaponManager] AddWeapon: definition or prefab is null.");
return -1;
}
// Already registered?
int existing = slots.FindIndex(s => s.definition == def);
if (existing >= 0) return existing;
GameObject instance = Instantiate(def.weaponPrefab, weaponHolder);
var vm = instance.GetComponent<WeaponViewmodel>();
if (vm != null)
vm.Apply();
else
{
instance.transform.localPosition = weaponPositionOffset;
instance.transform.localRotation = Quaternion.Euler(weaponRotationOffset);
instance.transform.localScale = weaponScale;
}
instance.SetActive(false);
// Strip colliders so they don't mess with the CharacterController
foreach (Collider col in instance.GetComponentsInChildren<Collider>())
if (!(col is CharacterController)) Destroy(col);
slots.Add(new WeaponSlot { itemName = def.itemName, instance = instance, definition = def });
int newIdx = slots.Count - 1;
Debug.Log($"[WeaponManager] Added weapon: {def.itemName} at slot {newIdx}");
return newIdx;
}
/// <summary>Equip weapon by slot index.</summary>
public void EquipByIndex(int index)
{
if (index < 0 || index >= slots.Count) return;
// Deactivate current
if (activeIndex >= 0 && activeIndex < slots.Count)
slots[activeIndex].instance.SetActive(false);
activeIndex = index;
slots[activeIndex].instance.SetActive(true);
Debug.Log($"[WeaponManager] Equipped: {slots[activeIndex].itemName}");
}
/// <summary>Equip weapon by item name.</summary>
public void EquipByName(string name)
{
int idx = slots.FindIndex(s => s.itemName == name);
if (idx >= 0)
EquipByIndex(idx);
else
Debug.LogWarning($"[WeaponManager] No slot found for: {name}");
}
/// <summary>Cycle forward (+1) or backward (-1).</summary>
public void CycleWeapon(int dir)
{
if (slots.Count == 0) return;
int next = (activeIndex + dir + slots.Count) % slots.Count;
EquipByIndex(next);
}
/// <summary>Deactivate the current weapon without switching to another.</summary>
public void Unequip()
{
if (activeIndex >= 0 && activeIndex < slots.Count)
slots[activeIndex].instance.SetActive(false);
activeIndex = -1;
Debug.Log("[WeaponManager] Unequipped all weapons.");
}
public string ActiveWeaponName =>
(activeIndex >= 0 && activeIndex < slots.Count) ? slots[activeIndex].itemName : "";
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 18715d388d5da7544b10c5d7a385cbee

View File

@@ -0,0 +1,29 @@
using UnityEngine;
/// <summary>
/// Add to a weapon prefab to define how it sits in the player's hands.
/// WeaponManager reads these values when the weapon is equipped.
/// </summary>
public class WeaponViewmodel : MonoBehaviour
{
[Header("View Model Transform")]
public Vector3 positionOffset = new Vector3(0.2f, -0.25f, 0.45f);
public Vector3 rotationOffset = new Vector3(0f, 0f, 0f);
public Vector3 scale = new Vector3(1f, 1f, 1f);
// Called by WeaponManager to apply these values
public void Apply()
{
transform.localPosition = positionOffset;
transform.localRotation = Quaternion.Euler(rotationOffset);
transform.localScale = scale;
}
// Called by WeaponManager to sync back after gizmo editing
public void SyncFromTransform()
{
positionOffset = transform.localPosition;
rotationOffset = transform.localRotation.eulerAngles;
scale = transform.localScale;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 59433c687536a60418803799027ddcc3

353
DEV_NOTES.md Normal file
View File

@@ -0,0 +1,353 @@
# OGG — Dev Notes & Systems Guide
> Boomer shooter. Cruelty Squad vibes. Jank is a feature.
> This doc covers all current systems.
---
## Scene Hierarchy
```
Player [CharacterController, FirstPersonController, SimpleCrosshair]
│ [Player, PlayerHUD, Inventory, WeaponManager, BootsEffect]
└── Camera [Camera, AudioListener, CameraShake, WeaponBob]
```
Pickups are standalone GameObjects placed anywhere in the scene — **not** children of the player.
---
## Scripts Overview
| Script | Lives On | What it does |
|---|---|---|
| `FirstPersonController` | Player | WASD move, mouse look, jump, sprint, cursor lock |
| `Player` | Player | Health + stamina stats, drain/regen logic, sprint gating |
| `PlayerHUD` | Player | Health bar, stamina bar, speedometer (IMGUI) |
| `SimpleGun` | Weapon prefabs | Shooting, ammo, recoil, muzzle flash, bullet decals |
| `CameraShake` | Camera | Singleton shake impulses, called by SimpleGun on fire |
| `WeaponBob` | Camera | Procedural weapon sway while moving |
| `WeaponViewmodel` | Weapon prefabs | Per-weapon position/rotation/scale in the player's hands |
| `SimpleCrosshair` | Player | Draws the crosshair via OnGUI |
| `EnemyHealth` | Enemy objects | Takes damage from SimpleGun raycasts, flashes red, dies |
| `EnemySpawner` | Any object | Spawns enemies at runtime |
| `HumanoidEnemy` | Enemy objects | Enemy AI / behaviour |
| `ItemDefinition` | ScriptableObject asset | Data definition for any item: name, icon, type, weapon prefab, equippable flag |
| `PickupItem` | Pickup GameObjects | Spin/bob animation, proximity prompt, E-to-collect |
| `Inventory` | Player | Item list, [I] HUD, right-click context menu, equip logic |
| `WeaponManager` | Player | Instantiates weapon prefabs, handles slot switching |
| `BootsEffect` | Player | Watches inventory for Bunny Hop Boots equipped state, applies stamina boost |
| `StaminaBoostPickup` | Pickup GameObjects | Legacy — superseded by BootsEffect. Left in for reference. |
---
## 1. Player Stats — `Player` + `PlayerHUD`
`Player.cs` is the data container for all player stats. `PlayerHUD.cs` reads from it and draws the HUD.
**Health fields:**
- `maxHealth` / `health` — enemies subtract from `health` directly, no auto-regen
**Stamina fields:**
- `maxStamina` / `stamina` — depletes while sprinting
- `staminaDrain` — units lost per second while sprinting (default 22)
- `staminaRegen` — units gained per second when resting (default 12)
- `regenDelay` — seconds of non-sprinting before regen starts (default 1.2)
- When stamina hits 0 the player is **exhausted** — sprint locked out until stamina fully refills
**HUD Layout:**
```
[bottom-left corner]
SPEED
42.3
units / sec
HEALTH 85 / 100
[████████████░░░]
STAMINA 62 / 100
[██████░░░░░░░░░]
```
**Speedometer:**
- Sits above the health/stamina bars, bottom-left
- Speed calculated from actual position delta per frame (horizontal only)
- Colour shifts: green → yellow → red as speed increases
- Thresholds configurable in Inspector (`speedoFastThreshold`, `speedoCritThreshold`)
- Can be toggled off via `showSpeedometer` bool
---
## 2. Movement — `FirstPersonController`
Standard FPS controller.
**Inspector fields:**
- `walkSpeed` / `runSpeed` — intentionally high (50/80) due to CharacterController collision tuning
- `mouseSensitivity` — linear, no acceleration
- `playerCamera` — auto-found if empty, best to assign explicitly
**Controls:**
| Key | Action |
|---|---|
| W A S D / Arrows | Move |
| Left Shift | Sprint (drains stamina) |
| Space | Jump |
| Mouse | Look |
| Escape | Unlock cursor |
| Left Click (unlocked) | Re-lock cursor |
---
## 3. Shooting — `SimpleGun`
Raycast-based hitscan. No projectiles.
**Key fields:**
- `damage` — per shot
- `range` — raycast distance in units
- `fireRate` — shots per second
- `isAutomatic` — hold vs click
- `recoilKickback` / `recoilKickUp` — feel tuning
- `fpsCam` — auto-found if null
**Internals:**
- Muzzle flash: child Quad + Point Light, shown ~50ms per shot
- Bullet decals: pooled (30 max), oldest reused when full
- Spark particles: spawned and self-destroyed on impact
- Calls `CameraShake.Instance.Shake()` on every shot
- Shooting blocked when inventory is open
- Position driven by `WeaponManager.weaponPositionOffset` each frame so recoil applies on top correctly
**Ammo:** `currentAmmo` / `maxAmmo`. Press **R** to reload (instant).
---
## 4. Item Definitions — `ItemDefinition` (ScriptableObject)
**Create:** Right-click in Project → **Create → OGG → Item Definition**
| Field | Notes |
|---|---|
| `itemName` | Display name everywhere — must be unique |
| `icon` | Sprite shown in inventory slots |
| `type` | `Misc`, `Consumable`, or `Weapon` |
| `isEquippable` | Tick for non-weapon items that can be equipped (e.g. boots) — shows equip button in inventory |
| `weaponPrefab` | Weapon type only — prefab spawned in weapon holder when equipped |
| `description` | Shown in inventory detail panel |
---
## 5. Pickups — `PickupItem`
Attach to any GameObject to make it a spinning, bobbing pickup.
**Setup:**
1. Place a GameObject in the scene with any mesh
2. Add `PickupItem` component
3. Drag an `ItemDefinition` into the `Definition` slot
**Inspector fields:**
| Field | Notes |
|---|---|
| `definition` | The ItemDefinition asset |
| `spinSpeed` | Degrees/sec Y rotation (default 120) |
| `bobHeight` | Vertical travel in units (default 0.18) |
| `bobSpeed` | Bob cycles per second (default 2.2) |
| `pickupRadius` | Proximity trigger distance |
| `pickupKey` | Default E |
| `pickupParticlesPrefab` | Optional — spawned on collect |
| `pickupSound` | Optional — played on collect |
**On pickup:** Calls `Inventory.AddItem(definition, autoEquip: true)` — picking up a weapon automatically equips it.
---
## 6. Inventory — `Inventory`
Lives on the Player. Press **I** to open/close. Cursor unlocks when open.
**HUD Layout:**
```
┌──────────────────────────────┐
│ [ INVENTORY ] │ ← header, shows active weapon top-right
├──────────────────────────────┤
│ [ ] [ ] [ ] [ ] │
│ [ ] [ ] [ ] [ ] │ ← item grid (4×5 by default)
├──────────────────────────────┤
│ SELECTED: Item Name │ ← detail panel
│ Description │
│ ▶ EQUIPPED [ F ] UNEQUIP│ ← shown for weapons AND equippable items
└──────────────────────────────┘
```
**Right-click context menu** on any slot:
- **Equip** / **Unequip** — shown for weapons and items with `isEquippable = true`
- **Inspect** — selects the item in the detail panel
- **Drop** — removes from inventory
**Controls:**
| Input | Action |
|---|---|
| I | Toggle inventory open/close |
| Click slot | Select item |
| Double-click weapon slot | Equip it |
| Right-click slot | Open context menu |
| F | Equip selected item |
| Scroll Wheel (closed) | Cycle weapons |
| 19 (closed) | Equip weapon by slot number |
**Slot colours:**
- Dark grey = empty
- Dark green = has item
- Bright green = selected
- Orange = equipped
**Useful methods:**
```csharp
inventory.HasItem("Medkit") // bool
inventory.CountOf("Grenade") // int
inventory.RemoveItem("Medkit", 1) // bool
inventory.AddItem(definition) // add by ItemDefinition
inventory.AddItem("Raw Name", sprite) // add by name
inventory.IsOpen // bool — checked by gun + WeaponManager
```
---
## 7. Weapon Manager — `WeaponManager`
Lives on the Player. Manages weapon instances and which is active.
**Setup:**
- Add `WeaponManager` to Player
- Set `Weapon Holder` to the Camera transform
- Any `SimpleGun` child of the holder is auto-registered but starts **inactive** — player must pick up and equip from inventory
**Weapon Position fields:**
- `weaponPositionOffset` / `weaponRotationOffset` / `weaponScale` — global fallback if the weapon has no `WeaponViewmodel` component
- Per-weapon positioning is preferred — add `WeaponViewmodel` to the weapon prefab instead
**Live positioning workflow (in Editor):**
1. Hit Play, equip the gun
2. On WeaponManager Inspector → click **"Select Active Weapon in Scene"**
3. Use gizmos in Scene view to position it
4. Click **"Sync Transform → Offset Fields"** — saves to the `WeaponViewmodel` on the prefab automatically
5. Stop Play — values persist
**Useful methods:**
```csharp
weaponManager.EquipByIndex(0)
weaponManager.EquipByName("Gun Splat")
weaponManager.CycleWeapon(1) // next
weaponManager.CycleWeapon(-1) // previous
weaponManager.ActiveWeaponName // string
weaponManager.Unequip() // holster all
```
---
## 8. WeaponViewmodel — `WeaponViewmodel`
Add to a **weapon prefab** to define how it sits in the player's hands. WeaponManager reads from it on equip instead of using the global offset.
**Fields:**
- `positionOffset` — local position relative to weapon holder
- `rotationOffset` — euler angles
- `scale` — local scale
Each weapon prefab gets its own values, so Gun Splat can sit differently from any future weapon.
---
## 9. Equippable Items — `BootsEffect`
**Add `BootsEffect` to the Player** for the Bunny Hop Boots to work.
Lives on the Player. Watches `Inventory.items` each frame for the boots' `isEquipped` state and adjusts `Player.maxStamina` accordingly.
| State | Effect |
|---|---|
| Boots equipped | `maxStamina` → 200, stamina topped up |
| Boots unequipped | `maxStamina` → 100, stamina clamped back down |
The stamina bar stretches/shrinks live to reflect the new max.
**Inspector fields:**
- `bootsItemName` — must match the ItemDefinition `itemName` exactly (default `"Bunny Hop Boots"`)
- `boostedMaxStamina` — default 200
---
## Setup Scripts (Editor Menu: OGG → Setup)
### Create Gun Splat Weapon
Creates:
- `Assets/Prefabs/Weapons/GunSplat.prefab` — held weapon with `SimpleGun`
- `Assets/Prefabs/Pickups/GunSplat_Pickup.prefab` — world pickup with spin/bob
- `Assets/Items/GunSplat_Item.asset``ItemDefinition` (type = Weapon)
### Create Bunny Hop Boots
Creates:
- `Assets/Items/BhopBoots_Item.asset``ItemDefinition` (type = Misc, isEquippable = true)
- `Assets/Prefabs/Pickups/BhopBoots_Pickup.prefab` — world pickup
**After running either script:** drag the pickup prefab into your scene.
---
## Adding a New Weapon — Step by Step
1. Create an `ItemDefinition` asset — set `type = Weapon`, assign your gun prefab to `weaponPrefab`
2. Add `SimpleGun` to the gun prefab root
3. Add `WeaponViewmodel` to the gun prefab — set position/rotation/scale or dial it in with the live Editor workflow
4. Create a pickup: any mesh + `PickupItem` component + drag in the ItemDefinition
5. Place the pickup in the scene
---
## Adding a New Equippable Item
1. Create an `ItemDefinition` — set `type = Misc`, tick `isEquippable = true`
2. Write a component that lives on the Player, watches `inventory.items` for `entry.isEquipped`, and applies/removes the effect
3. Place a pickup in the scene referencing the definition
---
## Adding a Consumable
```csharp
Inventory inv = GetComponent<Inventory>();
if (inv.HasItem("Medkit")) {
health += 50f;
inv.RemoveItem("Medkit");
}
```
---
## Known Rough Edges
- **No weapon switch animation** — instant swap. Hook into `WeaponManager.EquipByIndex()` when ready.
- **No save system** — inventory is runtime only, nothing persists between sessions.
- **Starting gun** — any `SimpleGun` that's a child of the Camera at Start gets auto-registered but starts inactive. Player needs to find it as a pickup in the scene.
- **Inventory is IMGUI** — not Canvas. Performance fine at this scale. All data logic is decoupled from drawing so a Canvas swap later would be straightforward.
- **`SimpleGun` assumes it's always active** — non-SimpleGun weapons will slot-switch fine but need their own input handling to shoot.
---
## Controls Reference
| Key | Action |
|---|---|
| W A S D | Move |
| Left Shift | Sprint |
| Space | Jump |
| Left Click | Shoot |
| Right Click | Context menu (inventory open) |
| R | Reload |
| E | Pick up nearby item |
| I | Open / close inventory |
| F | Equip selected item |
| Scroll Wheel | Cycle weapons |
| 1 9 | Equip weapon by slot |
| Escape | Unlock cursor |

View File

@@ -1,3 +1,4 @@
<Solution>
<Project Path="Assembly-CSharp.csproj" />
<Project Path="Assembly-CSharp-Editor.csproj" />
</Solution>