added inventory and stamina system
This commit is contained in:
46
Assets/Scripts/BootsEffect.cs
Normal file
46
Assets/Scripts/BootsEffect.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/BootsEffect.cs.meta
Normal file
2
Assets/Scripts/BootsEffect.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5680222822977cd4a988c93d7b8881bc
|
||||
8
Assets/Scripts/Editor.meta
Normal file
8
Assets/Scripts/Editor.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 384e9957bd5199549870be11d0838d80
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
100
Assets/Scripts/Editor/BhopBootsSetup.cs
Normal file
100
Assets/Scripts/Editor/BhopBootsSetup.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Editor/BhopBootsSetup.cs.meta
Normal file
2
Assets/Scripts/Editor/BhopBootsSetup.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 92ff81f98e809934bab9c5b8f160276a
|
||||
151
Assets/Scripts/Editor/GunSplatSetup.cs
Normal file
151
Assets/Scripts/Editor/GunSplatSetup.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Editor/GunSplatSetup.cs.meta
Normal file
2
Assets/Scripts/Editor/GunSplatSetup.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a0ceea0a64e3fa344b83004be51fe457
|
||||
92
Assets/Scripts/Editor/WeaponManagerEditor.cs
Normal file
92
Assets/Scripts/Editor/WeaponManagerEditor.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Editor/WeaponManagerEditor.cs.meta
Normal file
2
Assets/Scripts/Editor/WeaponManagerEditor.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fe67de8a32d4bf5449ee8def34a1bb8a
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
632
Assets/Scripts/Inventory.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Inventory.cs.meta
Normal file
2
Assets/Scripts/Inventory.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 514ba78e0d4cf8b4787471c2db0c6976
|
||||
28
Assets/Scripts/ItemDefinition.cs
Normal file
28
Assets/Scripts/ItemDefinition.cs
Normal 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 = "";
|
||||
}
|
||||
2
Assets/Scripts/ItemDefinition.cs.meta
Normal file
2
Assets/Scripts/ItemDefinition.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b549159f64e1b164082e2e4ae4d28ac1
|
||||
156
Assets/Scripts/PickupItem.cs
Normal file
156
Assets/Scripts/PickupItem.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/PickupItem.cs.meta
Normal file
2
Assets/Scripts/PickupItem.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 95709fa8fd6c75045b1ef8e4f505c152
|
||||
@@ -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
281
Assets/Scripts/PlayerHUD.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/PlayerHUD.cs.meta
Normal file
2
Assets/Scripts/PlayerHUD.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1d84d3e6c1cf6794da6ff84203edb291
|
||||
@@ -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;
|
||||
|
||||
36
Assets/Scripts/StaminaBoostPickup.cs
Normal file
36
Assets/Scripts/StaminaBoostPickup.cs
Normal 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/StaminaBoostPickup.cs.meta
Normal file
2
Assets/Scripts/StaminaBoostPickup.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1ce88c8ee6fd8a0469215a541f72f835
|
||||
197
Assets/Scripts/WeaponManager.cs
Normal file
197
Assets/Scripts/WeaponManager.cs
Normal 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 1–9
|
||||
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 : "";
|
||||
}
|
||||
2
Assets/Scripts/WeaponManager.cs.meta
Normal file
2
Assets/Scripts/WeaponManager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 18715d388d5da7544b10c5d7a385cbee
|
||||
29
Assets/Scripts/WeaponViewmodel.cs
Normal file
29
Assets/Scripts/WeaponViewmodel.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/WeaponViewmodel.cs.meta
Normal file
2
Assets/Scripts/WeaponViewmodel.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 59433c687536a60418803799027ddcc3
|
||||
Reference in New Issue
Block a user