using UnityEngine; /// /// Draws the player health and stamina bars. /// Attach to the Player GameObject alongside Player.cs /// /// Deliberately ugly. This is a Cruelty Squad game. /// 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(); _cc = GetComponent(); _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 ───────────────────────────────────────────────── /// Draws one labelled bar. Returns the Y position after the bar. 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; } }