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;
}
}