using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.InputSystem.Controls; public class PlayerController : MonoBehaviour { // Reference to the Player Input component private bool isJumpPressed = false; private float jumpPressTime = 0f; private bool _isLocalPlayer = false; public float maxJumpHoldTime = 0.5f; // Ex. limite la puissance du saut public float JumpForce = 5f; // Force applied when jumping public float MovementSpeed = 25f; // Speed of player movement public float BoostSpeed = 2f; // Multiplicateur de vitesse sur GelOrange [Header("Steering Feel")] [Tooltip("Damps velocity perpendicular to input — higher = sharper turns")] public float turnDamping = 1.5f; [Tooltip("Horizontal friction when no input is held")] public float idleDrag = 0.2f; [Header("Bump Collision")] public float bumpForce = 4f; // Impulse force when bumping a remote player public float bumpCooldown = 0.25f; // Minimum time between bumps from the same player private Dictionary _lastBumpTime = new Dictionary(); // Ajout des états pour chaque direction private bool isForwardHeld = false; private bool isBackwardsHeld = false; private bool isLeftHeld = false; private bool isRightHeld = false; private bool isOnGelOrange = false; // Indique si la boule est sur GelOrange private bool isOnGelViolet = false; // Indique si la boule est sur GelViolet (sticky) private Vector3 stickyNormal = Vector3.up; // Normale de la surface sticky en contact public float StickyForce = 20f; // Force qui plaque la balle contre la surface GelViolet private float originalDrag = 0f; // Sauvegarde du drag original du Rigidbody [Header("Limits")] public float maxVelocity = 120f; // Velocity cap to prevent infinite acceleration public float respawnY = -10f; // Y threshold for respawn private Vector3 _spawnPos = new Vector3(0f, 3f, -30f); private Rigidbody _rb; // Squash & stretch private bool _isSquashing = false; private Transform _meshTransform; // Reference to visual mesh for squash effect // Fall warning private static Texture2D _fallWarningTex; private float _fallWarningAlpha = 0f; public GameObject CameraReference; // Référence à la caméra (drag & drop dans l'inspecteur) // --- Local player floating name label --- private GameObject _nameLabelObj; private TextMesh _nameLabel; // --- Jump power (exposed for HUD) --- public bool IsJumpCharging => isJumpPressed && IsGrounded(); public float JumpChargeNormalized => Mathf.Clamp01(jumpPressTime / maxJumpHoldTime); // --- Shared font for TextMesh labels (WebGL-safe) --- private static Font _labelFont; public static Font LabelFont { get { if (_labelFont == null) _labelFont = Resources.Load("LiberationSans"); return _labelFont; } } void Start() { Debug.Log("PlayerController script initialized."); _rb = GetComponent(); _meshTransform = transform; } public void SetSpawnPosition(Vector3 pos) => _spawnPos = pos; /// /// Called by LobbyUI after connecting. Sets up the local player /// with a floating name label and a 50% color tint. /// public void SetupLocalPlayer(string playerName, Color playerColor) { _isLocalPlayer = true; // --- Color tint (multiply blend to keep original pattern visible) --- var ballRenderer = GetComponent(); if (ballRenderer != null) { var mat = new Material(ballRenderer.sharedMaterial); Color original = Color.white; if (mat.HasProperty("_BaseColor")) original = mat.GetColor("_BaseColor"); else if (mat.HasProperty("_Color")) original = mat.GetColor("_Color"); // Multiply tint: keeps the original pattern while colorizing // Strength 0.7 = strong color, 0.3 original preserved float strength = 0.7f; Color tint = new Color( Mathf.Lerp(original.r, original.r * playerColor.r * 2f, strength), Mathf.Lerp(original.g, original.g * playerColor.g * 2f, strength), Mathf.Lerp(original.b, original.b * playerColor.b * 2f, strength), original.a ); if (mat.HasProperty("_BaseColor")) mat.SetColor("_BaseColor", tint); if (mat.HasProperty("_Color")) mat.color = tint; ballRenderer.material = mat; } // --- Floating name label (parented to Player root, NOT the sphere) --- if (_nameLabelObj != null) Destroy(_nameLabelObj); _nameLabelObj = new GameObject("LocalNameLabel"); // Parent to the Player root (one level up) so ball rotation doesn't affect it _nameLabelObj.transform.SetParent(transform.parent, false); _nameLabelObj.transform.localScale = Vector3.one * 0.1f; _nameLabel = _nameLabelObj.AddComponent(); _nameLabel.text = playerName; _nameLabel.fontSize = 144; _nameLabel.characterSize = 0.15f; _nameLabel.anchor = TextAnchor.MiddleCenter; _nameLabel.alignment = TextAlignment.Center; _nameLabel.color = playerColor; if (LabelFont != null) _nameLabel.font = LabelFont; var renderer = _nameLabel.GetComponent(); if (LabelFont != null && LabelFont.material != null) renderer.material = LabelFont.material; else { var textShader = Shader.Find("GUI/Text Shader") ?? Shader.Find("Unlit/Texture"); if (textShader != null) renderer.material = new Material(textShader); } // --- Speed trail renderer --- var trail = gameObject.GetComponent(); if (trail == null) trail = gameObject.AddComponent(); trail.time = 0.4f; trail.startWidth = 0.3f; trail.endWidth = 0.02f; trail.material = new Material(Shader.Find("Sprites/Default")); trail.startColor = new Color(1f, 0.7f, 0.2f, 0.7f); trail.endColor = new Color(1f, 0.4f, 0.1f, 0f); trail.minVertexDistance = 0.1f; trail.autodestruct = false; trail.emitting = true; Debug.Log($"[Player] Local setup: {playerName}, tint={playerColor}"); } void LateUpdate() { // Keep local name label floating above the ball and facing the camera if (_nameLabelObj != null) { _nameLabelObj.transform.position = transform.position + Vector3.up * 1.5f; var cam = Camera.main; if (cam != null) { Vector3 lookDir = cam.transform.position - _nameLabelObj.transform.position; lookDir.y = 0f; if (lookDir.sqrMagnitude > 0.001f) _nameLabelObj.transform.rotation = Quaternion.LookRotation(lookDir); } } } // Update is called once per frame void Update() { // Cursor lock: right-click unlocks, left-click re-locks (disabled when any UI panel is open) if (!ChatUI.IsVisible && !KeyBindingUI.IsVisible && Mouse.current != null) { if (Cursor.lockState == CursorLockMode.Locked && Mouse.current.rightButton.wasPressedThisFrame) { Cursor.lockState = CursorLockMode.None; Cursor.visible = true; } else if (Cursor.lockState != CursorLockMode.Locked && Mouse.current.leftButton.wasPressedThisFrame) { Cursor.lockState = CursorLockMode.Locked; Cursor.visible = false; } } // Player update logic can be added here if (isJumpPressed) { jumpPressTime += Time.deltaTime; if (jumpPressTime > maxJumpHoldTime) { jumpPressTime = maxJumpHoldTime; // Clamp to max hold time } } // --- Respawn if fallen off the map --- if (transform.position.y < respawnY) { if (_rb != null) { _rb.linearVelocity = Vector3.zero; _rb.angularVelocity = Vector3.zero; _rb.useGravity = true; } transform.position = _spawnPos; isOnGelViolet = false; isOnGelOrange = false; Debug.Log("[Player] Respawned after falling."); return; } // --- Fall warning (tint screen red when low) --- float fallTarget = (transform.position.y < -3f) ? Mathf.Clamp01((-3f - transform.position.y) / 7f) : 0f; _fallWarningAlpha = Mathf.Lerp(_fallWarningAlpha, fallTarget, Time.deltaTime * 5f); // Mouvement continu selon les directions maintenues Rigidbody rb = _rb; if (rb != null) { float currentSpeed = MovementSpeed; if (isOnGelOrange) { currentSpeed *= BoostSpeed; } // Détermination des directions selon la caméra Vector3 forward = Vector3.forward; Vector3 right = Vector3.right; if (CameraReference != null) { Vector3 camForward = CameraReference.transform.forward; Vector3 camRight = CameraReference.transform.right; if (isOnGelViolet) { // PROJECT onto sticky surface plane for surface-relative movement forward = Vector3.ProjectOnPlane(camForward, stickyNormal).normalized; right = Vector3.ProjectOnPlane(camRight, stickyNormal).normalized; // Fallback if projection is degenerate if (forward.sqrMagnitude < 0.01f) forward = Vector3.ProjectOnPlane(Vector3.forward, stickyNormal).normalized; if (right.sqrMagnitude < 0.01f) right = Vector3.ProjectOnPlane(Vector3.right, stickyNormal).normalized; } else { // Normal mode: project onto horizontal plane camForward.y = 0; camForward.Normalize(); forward = camForward; camRight.y = 0; camRight.Normalize(); right = camRight; } } Vector3 inputDir = Vector3.zero; if (isForwardHeld) inputDir += forward; if (isBackwardsHeld) inputDir -= forward; if (isRightHeld) inputDir += right; if (isLeftHeld) inputDir -= right; if (inputDir.sqrMagnitude > 0.01f) { inputDir.Normalize(); rb.AddForce(inputDir * currentSpeed * Time.deltaTime, ForceMode.VelocityChange); // Counter-force on the lateral component (makes turns sharper) Vector3 horizVel = new Vector3(rb.linearVelocity.x, 0f, rb.linearVelocity.z); Vector3 perp = horizVel - Vector3.Project(horizVel, inputDir); if (perp.sqrMagnitude > 0.01f) rb.AddForce(-perp * turnDamping * Time.deltaTime, ForceMode.VelocityChange); } else if (!isOnGelViolet) { // Gradual horizontal slow-down when no key is held Vector3 horizVel = new Vector3(rb.linearVelocity.x, 0f, rb.linearVelocity.z); rb.AddForce(-horizVel * idleDrag * Time.deltaTime, ForceMode.VelocityChange); } // GelViolet : colle la balle à la surface (sticky) if (isOnGelViolet) { // Désactive la gravité Unity sur le Rigidbody rb.useGravity = false; // Applique une gravité inversée vers la surface rb.AddForce(-stickyNormal * Physics.gravity.magnitude, ForceMode.Acceleration); // Force de placage supplémentaire rb.AddForce(-stickyNormal * StickyForce, ForceMode.Acceleration); // Annule la vélocité qui s'éloigne de la surface (empêche le rebond) float velocityAwayFromSurface = Vector3.Dot(rb.linearVelocity, stickyNormal); if (velocityAwayFromSurface > 0) { rb.linearVelocity -= stickyNormal * velocityAwayFromSurface; } } else { rb.useGravity = true; } // --- Velocity cap --- if (rb.linearVelocity.magnitude > maxVelocity) { rb.linearVelocity = rb.linearVelocity.normalized * maxVelocity; } } } void OnCollisionStay(Collision collision) { Collider col = collision.collider; if (col == null || col.sharedMaterial == null) return; if (col.sharedMaterial.name.Contains("GelOrange")) { isOnGelOrange = true; } if (col.sharedMaterial.name.Contains("GelViolet")) { if (!isOnGelViolet) { originalDrag = _rb != null ? _rb.linearDamping : 0f; if (_rb != null) _rb.linearDamping = 1f; } isOnGelViolet = true; Vector3 avgNormal = Vector3.zero; foreach (ContactPoint contact in collision.contacts) avgNormal += contact.normal; stickyNormal = avgNormal.normalized; } } void OnCollisionExit(Collision collision) { Collider col = collision.collider; if (col != null && col.sharedMaterial != null) { if (col.sharedMaterial.name.Contains("GelOrange")) { isOnGelOrange = false; } if (col.sharedMaterial.name.Contains("GelViolet")) { isOnGelViolet = false; // Restaure le drag original et réactive la gravité Rigidbody rb = GetComponent(); if (rb != null) { rb.useGravity = true; rb.linearDamping = originalDrag; } } } } public void OnJump(InputAction.CallbackContext context) { if (ChatUI.IsVisible) { isJumpPressed = false; jumpPressTime = 0f; return; } if (context.started) { isJumpPressed = true; jumpPressTime = 0f; StatsTracker.Instance?.RegisterJump(); } else if (context.canceled) { float jumpForceFactor = Mathf.Clamp01(jumpPressTime / maxJumpHoldTime); if (IsGrounded()) PerformJump(jumpForceFactor * JumpForce); isJumpPressed = false; jumpPressTime = 0f; } } public void PerformJump(float force) { if (_rb == null) return; Vector3 jumpDir = isOnGelViolet ? stickyNormal : Vector3.up; _rb.AddForce(jumpDir * force, ForceMode.Impulse); } private bool IsGrounded() { // On sticky surface: raycast toward the surface (opposite of normal) if (isOnGelViolet) return Physics.Raycast(transform.position, -stickyNormal, 1.1f); // Normal: raycast downward return Physics.Raycast(transform.position, Vector3.down, 1.1f); } public void OnForward(InputAction.CallbackContext context) { if (ChatUI.IsVisible) { isForwardHeld = false; return; } if (context.started) isForwardHeld = true; else if (context.canceled) isForwardHeld = false; } public void OnBackwards(InputAction.CallbackContext context) { if (ChatUI.IsVisible) { isBackwardsHeld = false; return; } if (context.started) isBackwardsHeld = true; else if (context.canceled) isBackwardsHeld = false; } public void OnLeft(InputAction.CallbackContext context) { if (ChatUI.IsVisible) { isLeftHeld = false; return; } if (context.started) isLeftHeld = true; else if (context.canceled) isLeftHeld = false; } public void OnRight(InputAction.CallbackContext context) { if (ChatUI.IsVisible) { isRightHeld = false; return; } if (context.started) isRightHeld = true; else if (context.canceled) isRightHeld = false; } // --- Bump collision with remote players --- void OnTriggerEnter(Collider other) { HandleBump(other); } void OnTriggerStay(Collider other) { HandleBump(other); } private void HandleBump(Collider other) { var remote = other.GetComponent(); if (remote == null) return; int id = other.gameObject.GetInstanceID(); if (_lastBumpTime.TryGetValue(id, out float lastTime) && Time.time - lastTime < bumpCooldown) return; _lastBumpTime[id] = Time.time; StatsTracker.Instance?.RegisterBump(); // Repulsion direction: from remote toward local player Vector3 dir = (transform.position - other.transform.position).normalized; // Add slight upward component so the ball lifts off the ground dir = (dir + Vector3.up * 0.3f).normalized; if (_rb != null) _rb.AddForce(dir * bumpForce, ForceMode.Impulse); } void OnCollisionEnter(Collision collision) { if (!_isLocalPlayer) return; // Squash & stretch on landing/impact if (collision.relativeVelocity.magnitude > 2f && !_isSquashing) { StartCoroutine(SquashStretch()); } } private IEnumerator SquashStretch() { _isSquashing = true; Vector3 original = Vector3.one; Vector3 squash = new Vector3(1.2f, 0.7f, 1.2f); float t = 0f; float dur = 0.08f; // Squash while (t < dur) { t += Time.deltaTime; transform.localScale = Vector3.Lerp(original, squash, t / dur); yield return null; } // Stretch back t = 0f; dur = 0.12f; while (t < dur) { t += Time.deltaTime; transform.localScale = Vector3.Lerp(squash, original, t / dur); yield return null; } transform.localScale = original; _isSquashing = false; } public void ResetInputs() { isForwardHeld = false; isBackwardsHeld = false; isLeftHeld = false; isRightHeld = false; isJumpPressed = false; jumpPressTime = 0f; } void OnDestroy() { // Clean up name label (it's not parented to the ball) if (_nameLabelObj != null) Destroy(_nameLabelObj); } // ======================== // JUMP POWER GAUGE (HUD) // ======================== private static Texture2D _gaugeBarTex; private static Texture2D _gaugeBgTex; private static Texture2D _gaugeGlowTex; private float _gaugeDisplayAlpha = 0f; // smooth fade-in/out private float _lastChargeValue = 0f; // for smooth lerp void OnGUI() { if (!_isLocalPlayer) return; // Target alpha: 1 when charging, 0 otherwise (keep visible briefly after release) float targetAlpha = IsJumpCharging ? 1f : 0f; _gaugeDisplayAlpha = Mathf.MoveTowards(_gaugeDisplayAlpha, targetAlpha, Time.deltaTime * 6f); // Smooth the charge value for a fluid bar animation float chargeTarget = IsJumpCharging ? JumpChargeNormalized : 0f; _lastChargeValue = Mathf.Lerp(_lastChargeValue, chargeTarget, Time.deltaTime * 12f); if (_gaugeDisplayAlpha < 0.01f) return; // Gauge dimensions float barWidth = 400f; float barHeight = 22f; float x = (Screen.width - barWidth) / 2f; float y = Screen.height - 80f; // Label var labelStyle = new GUIStyle(GUI.skin.label) { alignment = TextAnchor.MiddleCenter, fontSize = 12, fontStyle = FontStyle.Bold }; labelStyle.normal.textColor = new Color(1f, 1f, 1f, _gaugeDisplayAlpha * 0.9f); // Outline: draw 4× in black at ±1px, then once in white var shadowStyle = new GUIStyle(labelStyle); shadowStyle.normal.textColor = new Color(0f, 0f, 0f, _gaugeDisplayAlpha * 0.55f); GUI.Label(new Rect(x + 1f, y - 25f, barWidth, 24f), "JUMP POWER", shadowStyle); GUI.Label(new Rect(x - 1f, y - 27f, barWidth, 24f), "JUMP POWER", shadowStyle); GUI.Label(new Rect(x + 1f, y - 27f, barWidth, 24f), "JUMP POWER", shadowStyle); GUI.Label(new Rect(x - 1f, y - 25f, barWidth, 24f), "JUMP POWER", shadowStyle); GUI.Label(new Rect(x, y - 26f, barWidth, 24f), "JUMP POWER", labelStyle); // Ensure textures if (_gaugeBgTex == null) { _gaugeBgTex = new Texture2D(1, 1); _gaugeBgTex.SetPixel(0, 0, new Color(0.08f, 0.08f, 0.12f, 1f)); _gaugeBgTex.Apply(); } if (_gaugeBarTex == null) { _gaugeBarTex = new Texture2D(1, 1); _gaugeBarTex.SetPixel(0, 0, Color.white); _gaugeBarTex.Apply(); } if (_gaugeGlowTex == null) { _gaugeGlowTex = new Texture2D(1, 1); _gaugeGlowTex.SetPixel(0, 0, Color.white); _gaugeGlowTex.Apply(); } Color prevBg = GUI.backgroundColor; Color prevColor = GUI.color; // Background (dark panel) float pad = 4f; Rect bgRect = new Rect(x - pad, y - pad, barWidth + pad * 2f, barHeight + pad * 2f); GUI.color = new Color(1f, 1f, 1f, _gaugeDisplayAlpha * 0.85f); GUI.DrawTexture(bgRect, _gaugeBgTex); // Border float b = 1f; Color borderColor = new Color(0.35f, 0.35f, 0.45f, _gaugeDisplayAlpha * 0.7f); GUI.color = borderColor; GUI.DrawTexture(new Rect(bgRect.x, bgRect.y, bgRect.width, b), _gaugeBarTex); // top GUI.DrawTexture(new Rect(bgRect.x, bgRect.yMax - b, bgRect.width, b), _gaugeBarTex); // bottom GUI.DrawTexture(new Rect(bgRect.x, bgRect.y, b, bgRect.height), _gaugeBarTex); // left GUI.DrawTexture(new Rect(bgRect.xMax - b, bgRect.y, b, bgRect.height), _gaugeBarTex); // right // Filled bar with gradient color (blue -> cyan -> yellow -> red) float fill = _lastChargeValue; Color barColor; if (fill < 0.5f) barColor = Color.Lerp(new Color(0.2f, 0.6f, 1f), new Color(0.1f, 0.95f, 0.85f), fill * 2f); else barColor = Color.Lerp(new Color(0.1f, 0.95f, 0.85f), new Color(1f, 0.3f, 0.2f), (fill - 0.5f) * 2f); Rect barRect = new Rect(x, y, barWidth * fill, barHeight); GUI.color = new Color(barColor.r, barColor.g, barColor.b, _gaugeDisplayAlpha); GUI.DrawTexture(barRect, _gaugeBarTex); // Glow overlay on the filled portion (bright center highlight) Color glowColor = new Color(1f, 1f, 1f, _gaugeDisplayAlpha * 0.2f * fill); GUI.color = glowColor; float glowH = barHeight * 0.4f; GUI.DrawTexture(new Rect(x, y + (barHeight - glowH) * 0.3f, barWidth * fill, glowH), _gaugeGlowTex); // Pulsing edge glow when near max if (fill > 0.85f) { float pulse = 0.4f + 0.6f * Mathf.Abs(Mathf.Sin(Time.time * 6f)); GUI.color = new Color(1f, 0.3f, 0.15f, _gaugeDisplayAlpha * 0.35f * pulse); GUI.DrawTexture(bgRect, _gaugeBarTex); } // Percentage text var pctStyle = new GUIStyle(GUI.skin.label) { alignment = TextAnchor.MiddleCenter, fontSize = 11, fontStyle = FontStyle.Bold }; pctStyle.normal.textColor = new Color(1f, 1f, 1f, _gaugeDisplayAlpha * 0.95f); GUI.Label(new Rect(x, y, barWidth, barHeight), Mathf.RoundToInt(fill * 100f) + "%", pctStyle); GUI.color = prevColor; GUI.backgroundColor = prevBg; // ======================== // FALL WARNING (red tint) // ======================== if (_fallWarningAlpha > 0.01f) { if (_fallWarningTex == null) { _fallWarningTex = new Texture2D(1, 1); _fallWarningTex.SetPixel(0, 0, Color.white); _fallWarningTex.Apply(); } GUI.color = new Color(0.9f, 0.1f, 0.05f, _fallWarningAlpha * 0.35f); GUI.DrawTexture(new Rect(0, 0, Screen.width, Screen.height), _fallWarningTex); // Warning text var warnStyle = new GUIStyle(GUI.skin.label) { alignment = TextAnchor.MiddleCenter, fontSize = 24, fontStyle = FontStyle.Bold }; warnStyle.normal.textColor = new Color(1f, 0.3f, 0.2f, _fallWarningAlpha); GUI.Label(new Rect(0, Screen.height * 0.35f, Screen.width, 40f), "\u26A0 DANGER - CHUTE !", warnStyle); } // ======================== // SPEED INDICATOR // ======================== if (_rb != null) { float speed = _rb.linearVelocity.magnitude; var speedStyle = new GUIStyle(GUI.skin.label) { alignment = TextAnchor.MiddleRight, fontSize = 12, fontStyle = FontStyle.Bold }; float speedAlpha = Mathf.Clamp01(speed / 5f) * 0.8f; Color speedCol = Color.Lerp(new Color(0.8f, 0.8f, 0.8f), new Color(1f, 0.5f, 0.1f), Mathf.Clamp01(speed / 30f)); speedStyle.normal.textColor = new Color(speedCol.r, speedCol.g, speedCol.b, Mathf.Max(0.3f, speedAlpha)); GUI.Label(new Rect(Screen.width - 160f, Screen.height - 50f, 140f, 24f), $"{speed:F1} m/s", speedStyle); } GUI.color = Color.white; } }