diff --git a/Assets/Scripts/BasketController.cs b/Assets/Scripts/BasketController.cs index b9c42a8..5ee06ff 100644 --- a/Assets/Scripts/BasketController.cs +++ b/Assets/Scripts/BasketController.cs @@ -6,9 +6,11 @@ public class BasketController : MonoBehaviour public ScoreManager scoreManager; [SerializeField] private Transform basketHoldPoint; [SerializeField] private LayerMask trickDetectMask = ~0; + [SerializeField] private float basketSpawnInterval = 3f; private readonly Collider2D[] overlapHits = new Collider2D[8]; private BoxCollider2D basketCollider; private TrickController currentTrick; + private float nextBasketSpawnTime; private void Awake() { @@ -18,6 +20,7 @@ public class BasketController : MonoBehaviour private void FixedUpdate() { RefreshTricksInBasket(); + TryBasketSpawn(); } private void OnCollisionEnter2D(Collision2D collision) @@ -33,10 +36,10 @@ public class BasketController : MonoBehaviour } RefreshTricksInBasket(); - if (currentTrick != null) - { - return; - } + if (currentTrick != null) + { + return; + } scoreManager.AddPoints(1); @@ -78,6 +81,7 @@ public class BasketController : MonoBehaviour currentTrick = trick; trick.SetBasketed(true, GetBasketAnchorPosition(trick)); + nextBasketSpawnTime = Time.time + basketSpawnInterval; } } @@ -94,6 +98,7 @@ public class BasketController : MonoBehaviour { currentTrick = null; trick.SetBasketed(false); + nextBasketSpawnTime = 0f; } } } @@ -124,6 +129,7 @@ public class BasketController : MonoBehaviour { currentTrick = trick; trick.SetBasketed(true, GetBasketAnchorPosition(trick)); + nextBasketSpawnTime = Time.time + basketSpawnInterval; } break; @@ -139,6 +145,7 @@ public class BasketController : MonoBehaviour { currentTrick.SetBasketed(false); currentTrick = null; + nextBasketSpawnTime = 0f; return; } @@ -162,9 +169,26 @@ public class BasketController : MonoBehaviour { currentTrick.SetBasketed(false); currentTrick = null; + nextBasketSpawnTime = 0f; } } + private void TryBasketSpawn() + { + if (currentTrick == null || basketSpawnInterval <= 0f) + { + return; + } + + if (Time.time < nextBasketSpawnTime) + { + return; + } + + TrickController.TrySpawnAt(currentTrick.transform.position); + nextBasketSpawnTime = Time.time + basketSpawnInterval; + } + private Vector2? GetBasketAnchorPosition(TrickController trick) { if (basketHoldPoint != null) diff --git a/Assets/Scripts/PlayerPickupController.cs b/Assets/Scripts/PlayerPickupController.cs index 4ad428f..9f3a08d 100644 --- a/Assets/Scripts/PlayerPickupController.cs +++ b/Assets/Scripts/PlayerPickupController.cs @@ -18,6 +18,7 @@ public class PlayerPickupController : MonoBehaviour [SerializeField] private float throwUpwardForce = 0f; [SerializeField] private float throwTriggerThreshold = 0.6f; [SerializeField] private float dropClearance = 0.2f; + [SerializeField] private float holdToSpawnTime = 3f; [Header("Setup")] public Transform holdPoint; // Empty GameObject above head @@ -33,6 +34,9 @@ public class PlayerPickupController : MonoBehaviour private PlayerStressController playerStressController; private bool isChargingThrow; private float throwChargeStartTime; + private float holdStartTime; + private bool spawnedDuringHold; + private float nextHoldSpawnTime; private void OnEnable() { @@ -113,6 +117,21 @@ public class PlayerPickupController : MonoBehaviour return; } + if (Time.time >= nextHoldSpawnTime) + { + if (heldObject == null) + { + return; + } + + if (!TrickController.TrySpawnFromPrefabAt(heldObject.transform.position)) + { + TrickController.TrySpawnAt(heldObject.transform.position); + } + spawnedDuringHold = true; + nextHoldSpawnTime = Time.time + holdToSpawnTime; + } + var gamepad = GetGamepad(); if (gamepad == null) { @@ -159,6 +178,9 @@ public class PlayerPickupController : MonoBehaviour } heldObject = col.gameObject; + holdStartTime = Time.time; + spawnedDuringHold = false; + nextHoldSpawnTime = holdStartTime + holdToSpawnTime; // Parent the object to the HoldPoint and reset its local position heldObject.transform.SetParent(holdPoint); @@ -260,6 +282,8 @@ public class PlayerPickupController : MonoBehaviour heldObject = null; heldBody = null; isChargingThrow = false; + spawnedDuringHold = false; + nextHoldSpawnTime = 0f; } private Gamepad GetGamepad() diff --git a/Assets/Scripts/TrickController.cs b/Assets/Scripts/TrickController.cs index e657385..2ae1218 100644 --- a/Assets/Scripts/TrickController.cs +++ b/Assets/Scripts/TrickController.cs @@ -11,6 +11,8 @@ public class TrickController : MonoBehaviour private static readonly List ActiveTricks = new List(); private static PlayerPlatformerController[] cachedPlayers = Array.Empty(); private static float nextPlayerCacheTime; + private static bool hasCanonicalScale; + private static Vector3 canonicalScale; public static int ActiveCount => ActiveTricks.Count; [SerializeField] private string treatTag = "Interactable"; @@ -30,6 +32,12 @@ public class TrickController : MonoBehaviour [SerializeField] private float jumpChancePerSecond = 0.5f; [SerializeField] private float jumpArcChance = 0.5f; [SerializeField] private float jumpArcHorizontalImpulse = 2f; + [SerializeField] private float basketSeekRadius = 8f; + [SerializeField] private float basketSeekWeight = 0.9f; + [SerializeField] private float basketMinHeight = 0.5f; + [SerializeField] private float basketRefreshInterval = 0.6f; + [SerializeField] private float basketJumpChance = 0.35f; + [SerializeField] private float basketJumpHorizontalRange = 0.8f; [SerializeField] private LayerMask groundMask = ~0; [SerializeField] private float groundCheckDistance = 0.05f; [SerializeField] private float despawnMinX = -12f; @@ -39,10 +47,13 @@ public class TrickController : MonoBehaviour [SerializeField] private float throwControlLockTime = 0.35f; [SerializeField] private float wallCheckDistance = 0.1f; [SerializeField] private float wallAvoidDuration = 0.6f; + [SerializeField] private float wallAvoidCooldown = 0.2f; private Rigidbody2D body; private Transform target; private float nextTargetTime; + private Transform basketTarget; + private float nextBasketTime; private Vector2 wanderDirection; private float nextWanderTime; private bool isCarried; @@ -55,6 +66,7 @@ public class TrickController : MonoBehaviour private readonly RaycastHit2D[] wallHits = new RaycastHit2D[4]; private float wallAvoidUntil; private float wallAvoidDirection; + private float nextWallAvoidTime; private RigidbodyType2D basketBodyType; private float basketGravityScale; private bool basketSimulated; @@ -65,6 +77,7 @@ public class TrickController : MonoBehaviour private SpriteRenderer cachedRenderer; private string originalSortingLayer; private int originalSortingOrder; + private Vector3 originalLocalScale; public bool IsCarried => isCarried; public bool IsBasketed => isBasketed; @@ -78,6 +91,12 @@ public class TrickController : MonoBehaviour body.collisionDetectionMode = CollisionDetectionMode2D.Continuous; body.interpolation = RigidbodyInterpolation2D.Interpolate; cachedRenderer = GetComponent(); + originalLocalScale = transform.localScale; + if (!hasCanonicalScale) + { + canonicalScale = transform.localScale; + hasCanonicalScale = true; + } } private void OnEnable() @@ -131,6 +150,12 @@ public class TrickController : MonoBehaviour nextTargetTime = Time.time + targetRefreshInterval; } + if (Time.time >= nextBasketTime) + { + basketTarget = FindNearestBasketAbove(); + nextBasketTime = Time.time + basketRefreshInterval; + } + if (Time.time >= nextWanderTime) { wanderDirection = Random.insideUnitCircle.normalized; @@ -189,6 +214,15 @@ public class TrickController : MonoBehaviour desiredX += wanderDirection.x * wanderWeight; } + if (!hasTarget && basketTarget != null) + { + var basketDelta = (Vector2)basketTarget.position - (Vector2)transform.position; + if (basketDelta.y >= basketMinHeight && basketDelta.magnitude <= basketSeekRadius) + { + desiredX += Mathf.Sign(basketDelta.x) * basketSeekWeight; + } + } + // No trick-avoidance bias. if (Mathf.Abs(desiredX) < 0.01f) @@ -196,11 +230,15 @@ public class TrickController : MonoBehaviour desiredX = wanderDirection.x * 0.2f; } - var wallDirection = GetWallAvoidDirection(desiredX); - if (wallDirection != 0f) + if (Time.time >= nextWallAvoidTime) { - wallAvoidDirection = wallDirection; - wallAvoidUntil = Time.time + wallAvoidDuration; + var wallDirection = GetWallAvoidDirection(desiredX); + if (wallDirection != 0f) + { + wallAvoidDirection = wallDirection; + wallAvoidUntil = Time.time + wallAvoidDuration; + nextWallAvoidTime = Time.time + wallAvoidDuration + wallAvoidCooldown; + } } if (Time.time < wallAvoidUntil && Mathf.Abs(wallAvoidDirection) > 0.01f) @@ -241,6 +279,14 @@ public class TrickController : MonoBehaviour { continue; } + if (candidate.GetComponent() != null) + { + continue; + } + if (candidate.GetComponent() != null) + { + continue; + } if (!candidate.TryGetComponent(out var collider) || !collider.enabled) { @@ -258,6 +304,48 @@ public class TrickController : MonoBehaviour return best; } + private Transform FindNearestBasketAbove() + { + var baskets = FindObjectsByType(FindObjectsSortMode.None); + if (baskets == null || baskets.Length == 0) + { + return null; + } + + Transform best = null; + var bestDistance = float.MaxValue; + var origin = transform.position; + for (var i = 0; i < baskets.Length; i++) + { + var basket = baskets[i]; + if (basket == null || !basket.gameObject.activeInHierarchy) + { + continue; + } + + var basketPos = basket.transform.position; + var delta = (Vector2)basketPos - (Vector2)origin; + if (delta.y < basketMinHeight) + { + continue; + } + + var distance = delta.magnitude; + if (distance > basketSeekRadius) + { + continue; + } + + if (distance < bestDistance) + { + bestDistance = distance; + best = basket.transform; + } + } + + return best; + } + private void TryConsumeTreat() { if (isCarried) @@ -293,22 +381,130 @@ public class TrickController : MonoBehaviour } } - private void SpawnTrick(Vector3 position) + private bool SpawnTrick(Vector3 position) { if (maxTricks > 0 && ActiveTricks.Count >= maxTricks) { - return; + return false; } if (trickPrefab != null) { - Instantiate(trickPrefab, position, Quaternion.identity); - return; + var instance = Instantiate(trickPrefab, position, Quaternion.identity); + if (instance != null) + { + instance.ResetRuntimeState(canonicalScale); + } + return true; } - Instantiate(gameObject, position, Quaternion.identity); + var clone = Instantiate(gameObject, position, Quaternion.identity); + if (clone.TryGetComponent(out var spawned)) + { + spawned.ResetRuntimeState(canonicalScale); + } + return true; } + private void ResetRuntimeState(Vector3? forcedScale = null) + { + isCarried = false; + isBasketed = false; + hasBasketAnchor = false; + movementLockUntil = 0f; + wallAvoidUntil = 0f; + wallAvoidDirection = 0f; + if (forcedScale.HasValue) + { + transform.localScale = forcedScale.Value; + } + if (cachedRenderer != null) + { + if (!string.IsNullOrWhiteSpace(basketSortingLayer)) + { + cachedRenderer.sortingLayerName = originalSortingLayer; + } + cachedRenderer.sortingOrder = originalSortingOrder; + } + + body.simulated = true; + body.bodyType = RigidbodyType2D.Dynamic; + body.gravityScale = 1f; + body.linearVelocity = Vector2.zero; + body.angularVelocity = 0f; + if (TryGetComponent(out var collider)) + { + collider.enabled = true; + } + } + + public static bool TrySpawnAt(Vector3 position) + { + if (ActiveTricks.Count == 0) + { + return false; + } + + TrickController candidate = null; + TrickController fallback = null; + for (var i = 0; i < ActiveTricks.Count; i++) + { + var trick = ActiveTricks[i]; + if (trick == null) + { + continue; + } + + if (fallback == null) + { + fallback = trick; + } + + if (!trick.isCarried && !trick.isBasketed) + { + if (trick.TryGetComponent(out var collider) && collider.enabled) + { + candidate = trick; + break; + } + } + } + + if (candidate == null) + { + candidate = fallback; + } + + return candidate != null && candidate.SpawnTrick(position); + } + + public static bool TrySpawnFromPrefabAt(Vector3 position) + { + if (ActiveTricks.Count == 0) + { + return false; + } + + for (var i = 0; i < ActiveTricks.Count; i++) + { + var trick = ActiveTricks[i]; + if (trick == null) + { + continue; + } + + if (trick.trickPrefab == null) + { + continue; + } + + return trick.SpawnTrick(position); + } + + return false; + } + + private float GetAvoidPlayerDirection() { if (Time.time >= nextPlayerCacheTime) @@ -353,7 +549,7 @@ public class TrickController : MonoBehaviour private void TryJump(bool isMoving, float jumpDirection) { - if (!isMoving || Time.time < nextJumpTime) + if (Time.time < nextJumpTime) { return; } @@ -363,20 +559,43 @@ public class TrickController : MonoBehaviour return; } - if (body.linearVelocity.y > 0.01f) + var wantsBasketJump = false; + var basketDir = 0f; + if (basketTarget != null) { - return; + var delta = (Vector2)basketTarget.position - (Vector2)transform.position; + if (delta.y >= basketMinHeight && Mathf.Abs(delta.x) <= basketJumpHorizontalRange) + { + if (Random.value < basketJumpChance) + { + wantsBasketJump = true; + basketDir = Mathf.Sign(delta.x); + } + } } - var chance = jumpChancePerSecond * Time.fixedDeltaTime; - if (Random.value > chance) + if (!wantsBasketJump) + { + if (!isMoving) + { + return; + } + + var chance = jumpChancePerSecond * Time.fixedDeltaTime; + if (Random.value > chance) + { + return; + } + } + + if (body.linearVelocity.y > 0.01f) { return; } var velocity = body.linearVelocity; velocity.y = 0f; - var jumpDir = Mathf.Sign(jumpDirection); + var jumpDir = wantsBasketJump ? basketDir : Mathf.Sign(jumpDirection); if (jumpDir == 0f) { jumpDir = Random.value < 0.5f ? -1f : 1f;