Stash work
This commit is contained in:
@@ -182,7 +182,7 @@ namespace Core
|
||||
// Register settings with service locator
|
||||
if (playerSettings != null)
|
||||
{
|
||||
ServiceLocator.Register<IPlayerFollowerSettings>(playerSettings);
|
||||
ServiceLocator.Register<IPlayerMovementConfigs>(playerSettings);
|
||||
Logging.Debug("PlayerFollowerSettings registered successfully");
|
||||
}
|
||||
else
|
||||
|
||||
@@ -3,37 +3,76 @@
|
||||
namespace AppleHills.Core.Settings
|
||||
{
|
||||
/// <summary>
|
||||
/// Settings related to player and follower behavior
|
||||
/// Settings related to player and follower behavior.
|
||||
/// Implements IPlayerMovementConfigs to provide separate configurations for different movement contexts.
|
||||
/// </summary>
|
||||
[CreateAssetMenu(fileName = "PlayerFollowerSettings", menuName = "AppleHills/Settings/Player & Follower", order = 1)]
|
||||
public class PlayerFollowerSettings : BaseSettings, IPlayerFollowerSettings
|
||||
public class PlayerFollowerSettings : BaseSettings, IPlayerMovementConfigs
|
||||
{
|
||||
[Header("Default Player Movement (Overworld)")]
|
||||
[SerializeField] private PlayerMovementSettingsData defaultPlayerMovement = new PlayerMovementSettingsData();
|
||||
|
||||
[Header("Trash Maze - Pulver Movement")]
|
||||
[SerializeField] private PlayerMovementSettingsData trashMazeMovement = new PlayerMovementSettingsData();
|
||||
|
||||
[Header("Follower Settings")]
|
||||
[SerializeField] private FollowerSettingsData followerMovement = new FollowerSettingsData();
|
||||
|
||||
// IPlayerMovementConfigs implementation
|
||||
public IPlayerMovementSettings DefaultPlayerMovement => defaultPlayerMovement;
|
||||
public IPlayerMovementSettings TrashMazeMovement => trashMazeMovement;
|
||||
public IFollowerSettings FollowerMovement => followerMovement;
|
||||
|
||||
public override void OnValidate()
|
||||
{
|
||||
base.OnValidate();
|
||||
defaultPlayerMovement?.Validate();
|
||||
trashMazeMovement?.Validate();
|
||||
followerMovement?.Validate();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serializable data for player movement settings
|
||||
/// </summary>
|
||||
[System.Serializable]
|
||||
public class PlayerMovementSettingsData : IPlayerMovementSettings
|
||||
{
|
||||
[Header("Player Settings")]
|
||||
[SerializeField] private float moveSpeed = 5f;
|
||||
[SerializeField] private float moveAcceleration = 10000f;
|
||||
[SerializeField] private float maxAcceleration = 10000f;
|
||||
[SerializeField] private float stopDistance = 0.1f;
|
||||
[SerializeField] private bool useRigidbody = true;
|
||||
[SerializeField] private HoldMovementMode defaultHoldMovementMode = HoldMovementMode.Pathfinding;
|
||||
|
||||
[Header("Follower Settings")]
|
||||
public float MoveSpeed => moveSpeed;
|
||||
public float MaxAcceleration => maxAcceleration;
|
||||
public float StopDistance => stopDistance;
|
||||
public bool UseRigidbody => useRigidbody;
|
||||
public HoldMovementMode DefaultHoldMovementMode => defaultHoldMovementMode;
|
||||
|
||||
public void Validate()
|
||||
{
|
||||
moveSpeed = Mathf.Max(0.1f, moveSpeed);
|
||||
maxAcceleration = Mathf.Max(0.1f, maxAcceleration);
|
||||
stopDistance = Mathf.Max(0.01f, stopDistance);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serializable data for follower settings
|
||||
/// </summary>
|
||||
[System.Serializable]
|
||||
public class FollowerSettingsData : IFollowerSettings
|
||||
{
|
||||
[SerializeField] private float followDistance = 1.5f;
|
||||
[SerializeField] private float manualMoveSmooth = 8f;
|
||||
[SerializeField] private float thresholdFar = 2.5f;
|
||||
[SerializeField] private float thresholdNear = 0.5f;
|
||||
[SerializeField] private float stopThreshold = 0.1f;
|
||||
|
||||
[Header("Backend Settings")]
|
||||
[Tooltip("Technical parameters, not for design tuning")]
|
||||
[SerializeField] private float followUpdateInterval = 0.1f;
|
||||
[SerializeField] private float followerSpeedMultiplier = 1.2f;
|
||||
[SerializeField] private float heldIconDisplayHeight = 2.0f;
|
||||
|
||||
// IPlayerFollowerSettings implementation
|
||||
public float MoveSpeed => moveSpeed;
|
||||
public float MaxAcceleration => moveAcceleration;
|
||||
public float StopDistance => stopDistance;
|
||||
public bool UseRigidbody => useRigidbody;
|
||||
public HoldMovementMode DefaultHoldMovementMode => defaultHoldMovementMode;
|
||||
public float FollowDistance => followDistance;
|
||||
public float ManualMoveSmooth => manualMoveSmooth;
|
||||
public float ThresholdFar => thresholdFar;
|
||||
@@ -42,14 +81,17 @@ namespace AppleHills.Core.Settings
|
||||
public float FollowUpdateInterval => followUpdateInterval;
|
||||
public float FollowerSpeedMultiplier => followerSpeedMultiplier;
|
||||
public float HeldIconDisplayHeight => heldIconDisplayHeight;
|
||||
|
||||
public override void OnValidate()
|
||||
|
||||
public void Validate()
|
||||
{
|
||||
base.OnValidate();
|
||||
// Validate values
|
||||
moveSpeed = Mathf.Max(0.1f, moveSpeed);
|
||||
followDistance = Mathf.Max(0.1f, followDistance);
|
||||
manualMoveSmooth = Mathf.Max(0.1f, manualMoveSmooth);
|
||||
thresholdFar = Mathf.Max(0.1f, thresholdFar);
|
||||
thresholdNear = Mathf.Max(0.01f, thresholdNear);
|
||||
stopThreshold = Mathf.Max(0.01f, stopThreshold);
|
||||
followUpdateInterval = Mathf.Max(0.01f, followUpdateInterval);
|
||||
followerSpeedMultiplier = Mathf.Max(0.1f, followerSpeedMultiplier);
|
||||
heldIconDisplayHeight = Mathf.Max(0f, heldIconDisplayHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,18 +14,33 @@ namespace AppleHills.Core.Settings
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for player and follower settings
|
||||
/// Interface for player movement settings (used by all player controllers)
|
||||
/// </summary>
|
||||
public interface IPlayerFollowerSettings
|
||||
public interface IPlayerMovementSettings
|
||||
{
|
||||
// Player settings
|
||||
float MoveSpeed { get; }
|
||||
float MaxAcceleration { get; } // Added new property for player acceleration
|
||||
float MaxAcceleration { get; }
|
||||
float StopDistance { get; }
|
||||
bool UseRigidbody { get; }
|
||||
HoldMovementMode DefaultHoldMovementMode { get; }
|
||||
|
||||
// Follower settings
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Container interface that holds multiple player movement configurations.
|
||||
/// Allows different controllers to use the same base settings interface with different values.
|
||||
/// </summary>
|
||||
public interface IPlayerMovementConfigs
|
||||
{
|
||||
IPlayerMovementSettings DefaultPlayerMovement { get; }
|
||||
IPlayerMovementSettings TrashMazeMovement { get; }
|
||||
IFollowerSettings FollowerMovement { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for follower-specific settings (completely separate from player movement)
|
||||
/// </summary>
|
||||
public interface IFollowerSettings
|
||||
{
|
||||
float FollowDistance { get; }
|
||||
float ManualMoveSmooth { get; }
|
||||
float ThresholdFar { get; }
|
||||
|
||||
330
Assets/Scripts/Input/BasePlayerMovementController.cs
Normal file
330
Assets/Scripts/Input/BasePlayerMovementController.cs
Normal file
@@ -0,0 +1,330 @@
|
||||
using UnityEngine;
|
||||
using Pathfinding;
|
||||
using AppleHills.Core.Settings;
|
||||
using Core;
|
||||
using Core.Lifecycle;
|
||||
|
||||
namespace Input
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for player movement controllers.
|
||||
/// Handles tap-to-move and hold-to-move input with pathfinding or direct movement.
|
||||
/// Derived classes can override to add specialized behavior (e.g., shader updates).
|
||||
/// </summary>
|
||||
public abstract class BasePlayerMovementController : ManagedBehaviour, ITouchInputConsumer
|
||||
{
|
||||
[Header("Movement")]
|
||||
[SerializeField] protected float moveSpeed = 5f;
|
||||
|
||||
[Header("Collision Simulation")]
|
||||
[SerializeField] protected LayerMask obstacleMask;
|
||||
[SerializeField] protected float colliderRadius = 0.5f;
|
||||
|
||||
// Movement state
|
||||
protected Vector3 _targetPosition;
|
||||
protected Vector3 _directMoveVelocity;
|
||||
protected bool _isHolding;
|
||||
protected Vector2 _lastHoldPosition;
|
||||
protected Coroutine _pathfindingDragCoroutine;
|
||||
protected float _pathfindingDragUpdateInterval = 0.1f;
|
||||
|
||||
// Settings reference (populated by derived classes in LoadSettings)
|
||||
protected IPlayerMovementSettings _movementSettings;
|
||||
protected IPlayerMovementSettings Settings => _movementSettings;
|
||||
|
||||
// Abstract method for derived classes to load their specific settings
|
||||
protected abstract void LoadSettings();
|
||||
|
||||
// Movement tracking
|
||||
protected bool _isMoving;
|
||||
public bool IsMoving => _isMoving;
|
||||
public bool IsHolding => _isHolding;
|
||||
public event System.Action OnMovementStarted;
|
||||
public event System.Action OnMovementStopped;
|
||||
|
||||
// Components
|
||||
protected AIPath _aiPath;
|
||||
protected Animator _animator;
|
||||
protected Transform _artTransform;
|
||||
protected SpriteRenderer _spriteRenderer;
|
||||
|
||||
// Animation tracking
|
||||
protected Vector3 _lastDirectMoveDir = Vector3.right;
|
||||
public Vector3 LastDirectMoveDir => _lastDirectMoveDir;
|
||||
protected float _lastDirX;
|
||||
protected float _lastDirY = -1f;
|
||||
|
||||
protected LogVerbosity _logVerbosity = LogVerbosity.Warning;
|
||||
|
||||
internal override void OnManagedAwake()
|
||||
{
|
||||
base.OnManagedAwake();
|
||||
LoadSettings(); // Let derived class load appropriate settings
|
||||
InitializeComponents();
|
||||
}
|
||||
|
||||
internal override void OnManagedStart()
|
||||
{
|
||||
base.OnManagedStart();
|
||||
|
||||
// Register with InputManager
|
||||
if (InputManager.Instance != null)
|
||||
{
|
||||
InputManager.Instance.SetDefaultConsumer(this);
|
||||
Logging.Debug($"[{GetType().Name}] Registered as default input consumer");
|
||||
}
|
||||
|
||||
_logVerbosity = DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>().inputLogVerbosity;
|
||||
}
|
||||
|
||||
protected virtual void InitializeComponents()
|
||||
{
|
||||
_aiPath = GetComponent<AIPath>();
|
||||
_artTransform = transform.Find("CharacterArt");
|
||||
if (_artTransform != null)
|
||||
_animator = _artTransform.GetComponent<Animator>();
|
||||
else
|
||||
_animator = GetComponentInChildren<Animator>();
|
||||
|
||||
if (_artTransform != null)
|
||||
_spriteRenderer = _artTransform.GetComponent<SpriteRenderer>();
|
||||
if (_spriteRenderer == null)
|
||||
_spriteRenderer = GetComponentInChildren<SpriteRenderer>();
|
||||
}
|
||||
|
||||
protected virtual void Update()
|
||||
{
|
||||
UpdateMovementState();
|
||||
UpdateAnimation();
|
||||
}
|
||||
|
||||
#region ITouchInputConsumer Implementation
|
||||
|
||||
public virtual void OnTap(Vector2 worldPosition)
|
||||
{
|
||||
Logging.Debug($"[{GetType().Name}] OnTap at {worldPosition}");
|
||||
if (_aiPath != null)
|
||||
{
|
||||
_aiPath.enabled = true;
|
||||
_aiPath.canMove = true;
|
||||
_aiPath.isStopped = false;
|
||||
SetTargetPosition(worldPosition);
|
||||
_directMoveVelocity = Vector3.zero;
|
||||
_isHolding = false;
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void OnHoldStart(Vector2 worldPosition)
|
||||
{
|
||||
Logging.Debug($"[{GetType().Name}] OnHoldStart at {worldPosition}");
|
||||
_lastHoldPosition = worldPosition;
|
||||
_isHolding = true;
|
||||
|
||||
if (Settings.DefaultHoldMovementMode == HoldMovementMode.Pathfinding && _aiPath != null)
|
||||
{
|
||||
_aiPath.enabled = true;
|
||||
if (_pathfindingDragCoroutine != null) StopCoroutine(_pathfindingDragCoroutine);
|
||||
_pathfindingDragCoroutine = StartCoroutine(PathfindingDragUpdateCoroutine());
|
||||
}
|
||||
else // Direct movement
|
||||
{
|
||||
if (_aiPath != null) _aiPath.enabled = false;
|
||||
_directMoveVelocity = Vector3.zero;
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void OnHoldUpdate(Vector2 worldPosition)
|
||||
{
|
||||
if (!_isHolding) return;
|
||||
_lastHoldPosition = worldPosition;
|
||||
|
||||
if (Settings.DefaultHoldMovementMode == HoldMovementMode.Direct)
|
||||
{
|
||||
if (_aiPath != null && _aiPath.enabled) _aiPath.enabled = false;
|
||||
MoveDirectlyTo(worldPosition);
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void OnHoldMove(Vector2 worldPosition)
|
||||
{
|
||||
// Alias for OnHoldUpdate for interface compatibility
|
||||
OnHoldUpdate(worldPosition);
|
||||
}
|
||||
|
||||
public virtual void OnHoldEnd(Vector2 worldPosition)
|
||||
{
|
||||
Logging.Debug($"[{GetType().Name}] OnHoldEnd at {worldPosition}");
|
||||
_isHolding = false;
|
||||
_directMoveVelocity = Vector3.zero;
|
||||
|
||||
if (_aiPath != null && Settings.DefaultHoldMovementMode == HoldMovementMode.Pathfinding)
|
||||
{
|
||||
if (_pathfindingDragCoroutine != null)
|
||||
{
|
||||
StopCoroutine(_pathfindingDragCoroutine);
|
||||
_pathfindingDragCoroutine = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (_aiPath != null && Settings.DefaultHoldMovementMode == HoldMovementMode.Direct)
|
||||
{
|
||||
_aiPath.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Movement Methods
|
||||
|
||||
protected virtual void SetTargetPosition(Vector2 worldPosition)
|
||||
{
|
||||
if (_aiPath != null)
|
||||
{
|
||||
_aiPath.destination = worldPosition;
|
||||
_aiPath.maxSpeed = Settings.MoveSpeed;
|
||||
_aiPath.maxAcceleration = Settings.MaxAcceleration;
|
||||
_aiPath.canMove = true;
|
||||
_aiPath.isStopped = false;
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void MoveDirectlyTo(Vector2 worldPosition)
|
||||
{
|
||||
if (_aiPath == null) return;
|
||||
|
||||
Vector3 current = transform.position;
|
||||
Vector3 target = new Vector3(worldPosition.x, worldPosition.y, current.z);
|
||||
Vector3 toTarget = (target - current);
|
||||
Vector3 direction = toTarget.normalized;
|
||||
|
||||
float maxSpeed = Settings.MoveSpeed;
|
||||
float acceleration = Settings.MaxAcceleration;
|
||||
|
||||
_directMoveVelocity = Vector3.MoveTowards(_directMoveVelocity, direction * maxSpeed, acceleration * Time.deltaTime);
|
||||
if (_directMoveVelocity.magnitude > maxSpeed)
|
||||
{
|
||||
_directMoveVelocity = _directMoveVelocity.normalized * maxSpeed;
|
||||
}
|
||||
|
||||
Vector3 move = _directMoveVelocity * Time.deltaTime;
|
||||
if (move.magnitude > toTarget.magnitude)
|
||||
{
|
||||
move = toTarget;
|
||||
}
|
||||
|
||||
// Collision simulation
|
||||
Vector3 adjustedVelocity = AdjustVelocityForObstacles(current, _directMoveVelocity);
|
||||
Vector3 adjustedMove = adjustedVelocity * Time.deltaTime;
|
||||
if (adjustedMove.magnitude > toTarget.magnitude)
|
||||
{
|
||||
adjustedMove = toTarget;
|
||||
}
|
||||
|
||||
transform.position += adjustedMove;
|
||||
_lastDirectMoveDir = _directMoveVelocity.normalized;
|
||||
}
|
||||
|
||||
protected virtual Vector3 AdjustVelocityForObstacles(Vector3 position, Vector3 velocity)
|
||||
{
|
||||
if (velocity.sqrMagnitude < 0.0001f)
|
||||
return velocity;
|
||||
|
||||
float moveDistance = velocity.magnitude * Time.deltaTime;
|
||||
Vector2 origin = new Vector2(position.x, position.y);
|
||||
Vector2 dir = velocity.normalized;
|
||||
float rayLength = colliderRadius + moveDistance;
|
||||
|
||||
RaycastHit2D hit = Physics2D.Raycast(origin, dir, rayLength, obstacleMask);
|
||||
|
||||
if (hit.collider != null)
|
||||
{
|
||||
Vector2 tangent = new Vector2(-hit.normal.y, hit.normal.x);
|
||||
float slideAmount = Vector2.Dot(velocity, tangent);
|
||||
Vector3 slideVelocity = tangent * slideAmount;
|
||||
return slideVelocity;
|
||||
}
|
||||
|
||||
return velocity;
|
||||
}
|
||||
|
||||
protected virtual System.Collections.IEnumerator PathfindingDragUpdateCoroutine()
|
||||
{
|
||||
while (_isHolding && _aiPath != null)
|
||||
{
|
||||
SetTargetPosition(_lastHoldPosition);
|
||||
yield return new WaitForSeconds(_pathfindingDragUpdateInterval);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region State and Animation
|
||||
|
||||
protected virtual void UpdateMovementState()
|
||||
{
|
||||
bool isCurrentlyMoving = false;
|
||||
|
||||
if (_isHolding && Settings.DefaultHoldMovementMode == HoldMovementMode.Direct)
|
||||
{
|
||||
isCurrentlyMoving = _directMoveVelocity.sqrMagnitude > 0.001f;
|
||||
}
|
||||
else if (_aiPath != null && _aiPath.enabled)
|
||||
{
|
||||
isCurrentlyMoving = _aiPath.velocity.sqrMagnitude > 0.001f;
|
||||
}
|
||||
|
||||
if (isCurrentlyMoving && !_isMoving)
|
||||
{
|
||||
_isMoving = true;
|
||||
OnMovementStarted?.Invoke();
|
||||
Logging.Debug($"[{GetType().Name}] Movement started");
|
||||
}
|
||||
else if (!isCurrentlyMoving && _isMoving)
|
||||
{
|
||||
_isMoving = false;
|
||||
OnMovementStopped?.Invoke();
|
||||
Logging.Debug($"[{GetType().Name}] Movement stopped");
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void UpdateAnimation()
|
||||
{
|
||||
if (_animator == null || _aiPath == null) return;
|
||||
|
||||
float normalizedSpeed = 0f;
|
||||
Vector3 velocity = Vector3.zero;
|
||||
float maxSpeed = Settings.MoveSpeed;
|
||||
|
||||
if (_isHolding && Settings.DefaultHoldMovementMode == HoldMovementMode.Direct)
|
||||
{
|
||||
normalizedSpeed = _directMoveVelocity.magnitude / maxSpeed;
|
||||
velocity = _directMoveVelocity;
|
||||
}
|
||||
else if (_aiPath.enabled)
|
||||
{
|
||||
normalizedSpeed = _aiPath.velocity.magnitude / maxSpeed;
|
||||
velocity = _aiPath.velocity;
|
||||
}
|
||||
|
||||
_animator.SetFloat("Speed", Mathf.Clamp01(normalizedSpeed));
|
||||
|
||||
if (velocity.sqrMagnitude > 0.01f)
|
||||
{
|
||||
Vector3 normalizedVelocity = velocity.normalized;
|
||||
_lastDirX = normalizedVelocity.x;
|
||||
_lastDirY = normalizedVelocity.y;
|
||||
|
||||
_animator.SetFloat("DirX", _lastDirX);
|
||||
_animator.SetFloat("DirY", _lastDirY);
|
||||
}
|
||||
else
|
||||
{
|
||||
_animator.SetFloat("DirX", _lastDirX);
|
||||
_animator.SetFloat("DirY", _lastDirY);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 059db1587c2c42389f98a1d3a52bec4b
|
||||
timeCreated: 1765206060
|
||||
@@ -1,8 +1,6 @@
|
||||
using UnityEngine;
|
||||
using Pathfinding;
|
||||
using AppleHills.Core.Settings;
|
||||
using Core;
|
||||
using Core.Lifecycle;
|
||||
|
||||
namespace Input
|
||||
{
|
||||
@@ -19,341 +17,43 @@ namespace Input
|
||||
/// <summary>
|
||||
/// Handles player movement in response to tap and hold input events.
|
||||
/// Supports both direct and pathfinding movement modes, and provides event/callbacks for arrival/cancellation.
|
||||
/// Extends BasePlayerMovementController with save/load and MoveToAndNotify functionality.
|
||||
/// </summary>
|
||||
public class PlayerTouchController : ManagedBehaviour, ITouchInputConsumer
|
||||
public class PlayerTouchController : BasePlayerMovementController
|
||||
{
|
||||
// --- Movement State ---
|
||||
private Vector3 targetPosition;
|
||||
private Vector3 directMoveVelocity; // default is Vector3.zero
|
||||
internal bool isHolding;
|
||||
private Vector2 lastHoldPosition;
|
||||
private Coroutine pathfindingDragCoroutine;
|
||||
private float pathfindingDragUpdateInterval = 0.1f; // Interval in seconds
|
||||
[Header("Collision Simulation")]
|
||||
public LayerMask obstacleMask;
|
||||
public float colliderRadius = 0.5f;
|
||||
|
||||
// --- Settings Reference ---
|
||||
private IPlayerFollowerSettings _settings;
|
||||
|
||||
// --- Movement Events ---
|
||||
private bool _isMoving = false;
|
||||
public bool IsMoving => _isMoving;
|
||||
public event System.Action OnMovementStarted;
|
||||
public event System.Action OnMovementStopped;
|
||||
|
||||
// --- Unity/Component References ---
|
||||
private AIPath aiPath;
|
||||
|
||||
// Note: String-based property lookup is flagged as inefficient, but is common in Unity for dynamic children.
|
||||
private Animator animator;
|
||||
private Transform artTransform;
|
||||
private SpriteRenderer spriteRenderer;
|
||||
|
||||
// --- Last direct movement direction ---
|
||||
private Vector3 _lastDirectMoveDir = Vector3.right;
|
||||
public Vector3 LastDirectMoveDir => _lastDirectMoveDir;
|
||||
|
||||
// --- Last movement directions for animation blend tree ---
|
||||
private float _lastDirX = 0f; // -1 (left) to 1 (right)
|
||||
private float _lastDirY = -1f; // -1 (down) to 1 (up)
|
||||
|
||||
// --- MoveToAndNotify State ---
|
||||
// --- PlayerTouchController-specific features (MoveToAndNotify) ---
|
||||
public delegate void ArrivedAtTargetHandler();
|
||||
private Coroutine moveToCoroutine;
|
||||
private Coroutine _moveToCoroutine;
|
||||
public event ArrivedAtTargetHandler OnArrivedAtTarget;
|
||||
public event System.Action OnMoveToCancelled;
|
||||
private bool interruptMoveTo;
|
||||
private LogVerbosity _logVerbosity = LogVerbosity.Warning;
|
||||
private bool _interruptMoveTo;
|
||||
|
||||
// Save system configuration
|
||||
public override bool AutoRegisterForSave => true;
|
||||
// Scene-specific SaveId - each level has its own player state
|
||||
public override string SaveId => $"{gameObject.scene.name}/PlayerController";
|
||||
|
||||
internal override void OnManagedStart()
|
||||
protected override void LoadSettings()
|
||||
{
|
||||
aiPath = GetComponent<AIPath>();
|
||||
artTransform = transform.Find("CharacterArt");
|
||||
if (artTransform != null)
|
||||
animator = artTransform.GetComponent<Animator>();
|
||||
else
|
||||
animator = GetComponentInChildren<Animator>();
|
||||
// Cache SpriteRenderer for flipping
|
||||
if (artTransform != null)
|
||||
spriteRenderer = artTransform.GetComponent<SpriteRenderer>();
|
||||
if (spriteRenderer == null)
|
||||
spriteRenderer = GetComponentInChildren<SpriteRenderer>();
|
||||
|
||||
// Initialize settings reference using GetSettingsObject
|
||||
_settings = GameManager.GetSettingsObject<IPlayerFollowerSettings>();
|
||||
|
||||
// Set default input consumer
|
||||
InputManager.Instance?.SetDefaultConsumer(this);
|
||||
|
||||
_logVerbosity = DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>().inputLogVerbosity;
|
||||
var configs = GameManager.GetSettingsObject<IPlayerMovementConfigs>();
|
||||
_movementSettings = configs.DefaultPlayerMovement;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles tap input. Always uses pathfinding to move to the tapped location.
|
||||
/// Cancels any in-progress MoveToAndNotify.
|
||||
/// </summary>
|
||||
public void OnTap(Vector2 worldPosition)
|
||||
#region ITouchInputConsumer Overrides (Add InterruptMoveTo)
|
||||
|
||||
public override void OnTap(Vector2 worldPosition)
|
||||
{
|
||||
InterruptMoveTo();
|
||||
Logging.Debug($"OnTap at {worldPosition}");
|
||||
if (aiPath != null)
|
||||
{
|
||||
aiPath.enabled = true;
|
||||
aiPath.canMove = true;
|
||||
aiPath.isStopped = false;
|
||||
SetTargetPosition(worldPosition);
|
||||
directMoveVelocity = Vector3.zero;
|
||||
isHolding = false;
|
||||
}
|
||||
base.OnTap(worldPosition);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the start of a hold input. Begins tracking the finger and uses the correct movement mode.
|
||||
/// Cancels any in-progress MoveToAndNotify.
|
||||
/// </summary>
|
||||
public void OnHoldStart(Vector2 worldPosition)
|
||||
|
||||
public override void OnHoldStart(Vector2 worldPosition)
|
||||
{
|
||||
InterruptMoveTo();
|
||||
Logging.Debug($"OnHoldStart at {worldPosition}");
|
||||
lastHoldPosition = worldPosition;
|
||||
isHolding = true;
|
||||
if (_settings.DefaultHoldMovementMode == HoldMovementMode.Pathfinding &&
|
||||
aiPath != null)
|
||||
{
|
||||
aiPath.enabled = true;
|
||||
if (pathfindingDragCoroutine != null) StopCoroutine(pathfindingDragCoroutine);
|
||||
pathfindingDragCoroutine = StartCoroutine(PathfindingDragUpdateCoroutine());
|
||||
}
|
||||
else // Direct movement
|
||||
{
|
||||
if (aiPath != null) aiPath.enabled = false;
|
||||
directMoveVelocity = Vector3.zero;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles hold move input. Updates the target position for direct or pathfinding movement.
|
||||
/// /// </summary>
|
||||
public void OnHoldMove(Vector2 worldPosition)
|
||||
{
|
||||
if (!isHolding) return;
|
||||
lastHoldPosition = worldPosition;
|
||||
if (_settings.DefaultHoldMovementMode == HoldMovementMode.Direct)
|
||||
{
|
||||
if (aiPath != null && aiPath.enabled) aiPath.enabled = false;
|
||||
MoveDirectlyTo(worldPosition);
|
||||
}
|
||||
// If pathfinding, coroutine will update destination
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the end of a hold input. Stops tracking and disables movement as needed.
|
||||
/// </summary>
|
||||
public void OnHoldEnd(Vector2 worldPosition)
|
||||
{
|
||||
Logging.Debug($"OnHoldEnd at {worldPosition}");
|
||||
isHolding = false;
|
||||
directMoveVelocity = Vector3.zero;
|
||||
if (aiPath != null && _settings.DefaultHoldMovementMode ==
|
||||
HoldMovementMode.Pathfinding)
|
||||
{
|
||||
if (pathfindingDragCoroutine != null)
|
||||
{
|
||||
StopCoroutine(pathfindingDragCoroutine);
|
||||
pathfindingDragCoroutine = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (aiPath != null && _settings.DefaultHoldMovementMode == HoldMovementMode.Direct)
|
||||
{
|
||||
aiPath.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the target position for pathfinding movement.
|
||||
/// </summary>
|
||||
private void SetTargetPosition(Vector2 worldPosition)
|
||||
{
|
||||
if (aiPath != null)
|
||||
{
|
||||
aiPath.destination = worldPosition;
|
||||
// Apply both speed and acceleration from settings
|
||||
aiPath.maxSpeed = _settings.MoveSpeed;
|
||||
aiPath.maxAcceleration = _settings.MaxAcceleration;
|
||||
aiPath.canMove = true;
|
||||
aiPath.isStopped = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves the player directly towards the specified world position.
|
||||
/// </summary>
|
||||
public void MoveDirectlyTo(Vector2 worldPosition)
|
||||
{
|
||||
if (aiPath == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Vector3 current = transform.position;
|
||||
Vector3 target = new Vector3(worldPosition.x, worldPosition.y, current.z);
|
||||
Vector3 toTarget = (target - current);
|
||||
Vector3 direction = toTarget.normalized;
|
||||
|
||||
// Get speed and acceleration directly from settings
|
||||
float maxSpeed = _settings.MoveSpeed;
|
||||
float acceleration = _settings.MaxAcceleration;
|
||||
|
||||
directMoveVelocity = Vector3.MoveTowards(directMoveVelocity, direction * maxSpeed, acceleration * Time.deltaTime);
|
||||
if (directMoveVelocity.magnitude > maxSpeed)
|
||||
{
|
||||
directMoveVelocity = directMoveVelocity.normalized * maxSpeed;
|
||||
}
|
||||
Vector3 move = directMoveVelocity * Time.deltaTime;
|
||||
if (move.magnitude > toTarget.magnitude)
|
||||
{
|
||||
move = toTarget;
|
||||
}
|
||||
// --- Collision simulation ---
|
||||
Vector3 adjustedVelocity = AdjustVelocityForObstacles(current, directMoveVelocity);
|
||||
Vector3 adjustedMove = adjustedVelocity * Time.deltaTime;
|
||||
if (adjustedMove.magnitude > toTarget.magnitude)
|
||||
{
|
||||
adjustedMove = toTarget;
|
||||
}
|
||||
transform.position += adjustedMove;
|
||||
|
||||
// Cache the last direct movement direction
|
||||
_lastDirectMoveDir = directMoveVelocity.normalized;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simulates collision with obstacles by raycasting in the direction of velocity and projecting the velocity if a collision is detected.
|
||||
/// </summary>
|
||||
/// <param name="position">Player's current position.</param>
|
||||
/// <param name="velocity">Intended velocity for this frame.</param>
|
||||
/// <returns>Adjusted velocity after collision simulation.</returns>
|
||||
private Vector3 AdjustVelocityForObstacles(Vector3 position, Vector3 velocity)
|
||||
{
|
||||
if (velocity.sqrMagnitude < 0.0001f)
|
||||
return velocity;
|
||||
float moveDistance = velocity.magnitude * Time.deltaTime;
|
||||
Vector2 origin = new Vector2(position.x, position.y);
|
||||
Vector2 dir = velocity.normalized;
|
||||
float rayLength = colliderRadius + moveDistance;
|
||||
RaycastHit2D hit = Physics2D.Raycast(origin, dir, rayLength, obstacleMask);
|
||||
Debug.DrawLine(origin, origin + dir * rayLength, Color.red, 0.1f);
|
||||
if (hit.collider != null)
|
||||
{
|
||||
// Draw normal and tangent for debug
|
||||
Debug.DrawLine(hit.point, hit.point + hit.normal, Color.green, 0.2f);
|
||||
Vector2 tangent = new Vector2(-hit.normal.y, hit.normal.x);
|
||||
Debug.DrawLine(hit.point, hit.point + tangent, Color.blue, 0.2f);
|
||||
// Project velocity onto tangent to simulate sliding
|
||||
float slideAmount = Vector2.Dot(velocity, tangent);
|
||||
Vector3 slideVelocity = tangent * slideAmount;
|
||||
return slideVelocity;
|
||||
}
|
||||
return velocity;
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
UpdateMovementState();
|
||||
|
||||
if (animator != null && aiPath != null)
|
||||
{
|
||||
float normalizedSpeed = 0f;
|
||||
Vector3 velocity = Vector3.zero;
|
||||
float maxSpeed = _settings.MoveSpeed;
|
||||
|
||||
if (isHolding && _settings.DefaultHoldMovementMode == HoldMovementMode.Direct)
|
||||
{
|
||||
normalizedSpeed = directMoveVelocity.magnitude / maxSpeed;
|
||||
velocity = directMoveVelocity;
|
||||
}
|
||||
else if (aiPath.enabled)
|
||||
{
|
||||
normalizedSpeed = aiPath.velocity.magnitude / maxSpeed;
|
||||
velocity = aiPath.velocity;
|
||||
}
|
||||
|
||||
// Set speed parameter as before
|
||||
animator.SetFloat("Speed", Mathf.Clamp01(normalizedSpeed));
|
||||
|
||||
// Calculate and set X and Y directions for 2D blend tree
|
||||
if (velocity.sqrMagnitude > 0.01f)
|
||||
{
|
||||
// Normalize the velocity vector to get direction
|
||||
Vector3 normalizedVelocity = velocity.normalized;
|
||||
|
||||
// Update the stored directions when actively moving
|
||||
_lastDirX = normalizedVelocity.x;
|
||||
_lastDirY = normalizedVelocity.y;
|
||||
|
||||
// Set the animator parameters
|
||||
animator.SetFloat("DirX", _lastDirX);
|
||||
animator.SetFloat("DirY", _lastDirY);
|
||||
}
|
||||
else
|
||||
{
|
||||
// When not moving, keep using the last direction
|
||||
animator.SetFloat("DirX", _lastDirX);
|
||||
animator.SetFloat("DirY", _lastDirY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the player is currently moving and fires appropriate events when movement state changes.
|
||||
/// </summary>
|
||||
private void UpdateMovementState()
|
||||
{
|
||||
bool isCurrentlyMoving = false;
|
||||
|
||||
// Check direct movement
|
||||
if (isHolding && _settings.DefaultHoldMovementMode == HoldMovementMode.Direct)
|
||||
{
|
||||
isCurrentlyMoving = directMoveVelocity.sqrMagnitude > 0.001f;
|
||||
}
|
||||
// Check pathfinding movement
|
||||
else if (aiPath != null && aiPath.enabled)
|
||||
{
|
||||
isCurrentlyMoving = aiPath.velocity.sqrMagnitude > 0.001f;
|
||||
}
|
||||
|
||||
// Fire events only when state changes
|
||||
if (isCurrentlyMoving && !_isMoving)
|
||||
{
|
||||
_isMoving = true;
|
||||
OnMovementStarted?.Invoke();
|
||||
Logging.Debug("Movement started");
|
||||
}
|
||||
else if (!isCurrentlyMoving && _isMoving)
|
||||
{
|
||||
_isMoving = false;
|
||||
OnMovementStopped?.Invoke();
|
||||
Logging.Debug("Movement stopped");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Coroutine for updating the AIPath destination during pathfinding hold movement.
|
||||
/// </summary>
|
||||
private System.Collections.IEnumerator PathfindingDragUpdateCoroutine()
|
||||
{
|
||||
while (isHolding && aiPath != null)
|
||||
{
|
||||
aiPath.destination = new Vector3(lastHoldPosition.x, lastHoldPosition.y, transform.position.z);
|
||||
yield return new WaitForSeconds(pathfindingDragUpdateInterval);
|
||||
}
|
||||
base.OnHoldStart(worldPosition);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Moves the player to a specific target position and notifies via events when arrived or cancelled.
|
||||
@@ -362,20 +62,20 @@ namespace Input
|
||||
public void MoveToAndNotify(Vector3 target)
|
||||
{
|
||||
// Cancel any previous move-to coroutine
|
||||
if (moveToCoroutine != null)
|
||||
if (_moveToCoroutine != null)
|
||||
{
|
||||
StopCoroutine(moveToCoroutine);
|
||||
StopCoroutine(_moveToCoroutine);
|
||||
}
|
||||
|
||||
interruptMoveTo = false;
|
||||
_interruptMoveTo = false;
|
||||
// Ensure pathfinding is enabled for MoveToAndNotify
|
||||
if (aiPath != null)
|
||||
if (_aiPath != null)
|
||||
{
|
||||
aiPath.enabled = true;
|
||||
aiPath.canMove = true;
|
||||
aiPath.isStopped = false;
|
||||
_aiPath.enabled = true;
|
||||
_aiPath.canMove = true;
|
||||
_aiPath.isStopped = false;
|
||||
}
|
||||
moveToCoroutine = StartCoroutine(MoveToTargetCoroutine(target));
|
||||
_moveToCoroutine = StartCoroutine(MoveToTargetCoroutine(target));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -383,11 +83,11 @@ namespace Input
|
||||
/// </summary>
|
||||
public void InterruptMoveTo()
|
||||
{
|
||||
interruptMoveTo = true;
|
||||
isHolding = false;
|
||||
directMoveVelocity = Vector3.zero;
|
||||
if (_settings.DefaultHoldMovementMode == HoldMovementMode.Direct && aiPath != null)
|
||||
aiPath.enabled = false;
|
||||
_interruptMoveTo = true;
|
||||
_isHolding = false;
|
||||
_directMoveVelocity = Vector3.zero;
|
||||
if (Settings.DefaultHoldMovementMode == HoldMovementMode.Direct && _aiPath != null)
|
||||
_aiPath.enabled = false;
|
||||
OnMoveToCancelled?.Invoke();
|
||||
}
|
||||
|
||||
@@ -396,19 +96,19 @@ namespace Input
|
||||
/// </summary>
|
||||
private System.Collections.IEnumerator MoveToTargetCoroutine(Vector3 target)
|
||||
{
|
||||
if (aiPath != null)
|
||||
if (_aiPath != null)
|
||||
{
|
||||
aiPath.destination = target;
|
||||
aiPath.maxSpeed = _settings.MoveSpeed;
|
||||
aiPath.maxAcceleration = _settings.MaxAcceleration;
|
||||
_aiPath.destination = target;
|
||||
_aiPath.maxSpeed = Settings.MoveSpeed;
|
||||
_aiPath.maxAcceleration = Settings.MaxAcceleration;
|
||||
}
|
||||
|
||||
while (!interruptMoveTo)
|
||||
while (!_interruptMoveTo)
|
||||
{
|
||||
Vector2 current2D = new Vector2(transform.position.x, transform.position.y);
|
||||
Vector2 target2D = new Vector2(target.x, target.y);
|
||||
float dist = Vector2.Distance(current2D, target2D);
|
||||
if (dist <= _settings.StopDistance + 0.2f)
|
||||
if (dist <= Settings.StopDistance + 0.2f)
|
||||
{
|
||||
break;
|
||||
}
|
||||
@@ -416,8 +116,8 @@ namespace Input
|
||||
yield return null;
|
||||
}
|
||||
|
||||
moveToCoroutine = null;
|
||||
if (!interruptMoveTo)
|
||||
_moveToCoroutine = null;
|
||||
if (!_interruptMoveTo)
|
||||
{
|
||||
OnArrivedAtTarget?.Invoke();
|
||||
}
|
||||
|
||||
@@ -64,7 +64,6 @@ namespace Interactions
|
||||
|
||||
// Settings reference
|
||||
private IInteractionSettings interactionSettings;
|
||||
private IPlayerFollowerSettings playerFollowerSettings;
|
||||
|
||||
/// <summary>
|
||||
/// Read-only access to the current slotted item state.
|
||||
@@ -180,7 +179,6 @@ namespace Interactions
|
||||
|
||||
// Initialize settings references
|
||||
interactionSettings = GameManager.GetSettingsObject<IInteractionSettings>();
|
||||
playerFollowerSettings = GameManager.GetSettingsObject<IPlayerFollowerSettings>();
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
@@ -580,7 +578,8 @@ namespace Interactions
|
||||
slotRenderer.sprite = slottedData.mapSprite;
|
||||
|
||||
// Scale sprite to desired height, preserve aspect ratio, compensate for parent scale
|
||||
float desiredHeight = playerFollowerSettings?.HeldIconDisplayHeight ?? 2.0f;
|
||||
var configs = GameManager.GetSettingsObject<IPlayerMovementConfigs>();
|
||||
float desiredHeight = configs?.FollowerMovement?.HeldIconDisplayHeight ?? 2.0f;
|
||||
var sprite = slottedData.mapSprite;
|
||||
float spriteHeight = sprite.bounds.size.y;
|
||||
Vector3 parentScale = slotRenderer.transform.parent != null
|
||||
|
||||
9
Assets/Scripts/Minigames/TrashMaze.meta
Normal file
9
Assets/Scripts/Minigames/TrashMaze.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
9
Assets/Scripts/Minigames/TrashMaze/Core.meta
Normal file
9
Assets/Scripts/Minigames/TrashMaze/Core.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
87
Assets/Scripts/Minigames/TrashMaze/Core/PulverController.cs
Normal file
87
Assets/Scripts/Minigames/TrashMaze/Core/PulverController.cs
Normal file
@@ -0,0 +1,87 @@
|
||||
using Core;
|
||||
using UnityEngine;
|
||||
using Input;
|
||||
using AppleHills.Core.Settings;
|
||||
|
||||
namespace Minigames.TrashMaze.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Controls Pulver character movement in the Trash Maze.
|
||||
/// Inherits from BasePlayerMovementController for tap-to-move and hold-to-move.
|
||||
/// Updates global shader properties for vision radius system.
|
||||
/// </summary>
|
||||
public class PulverController : BasePlayerMovementController
|
||||
{
|
||||
public static PulverController Instance { get; private set; }
|
||||
|
||||
[Header("Vision")]
|
||||
[SerializeField] private float visionRadius = 3f;
|
||||
|
||||
// Cached shader property IDs for performance
|
||||
private static readonly int PlayerWorldPosID = Shader.PropertyToID("_PlayerWorldPos");
|
||||
private static readonly int VisionRadiusID = Shader.PropertyToID("_VisionRadius");
|
||||
|
||||
// Public accessors for other systems
|
||||
public static Vector2 PlayerPosition => Instance != null ? Instance.transform.position : Vector2.zero;
|
||||
public static float VisionRadius => Instance != null ? Instance.visionRadius : 3f;
|
||||
|
||||
internal override void OnManagedAwake()
|
||||
{
|
||||
// Singleton pattern
|
||||
if (Instance != null && Instance != this)
|
||||
{
|
||||
Logging.Warning("[PulverController] Duplicate instance detected. Destroying duplicate.");
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
Instance = this;
|
||||
|
||||
base.OnManagedAwake();
|
||||
|
||||
Logging.Debug("[PulverController] Initialized");
|
||||
}
|
||||
|
||||
protected override void LoadSettings()
|
||||
{
|
||||
var configs = GameManager.GetSettingsObject<IPlayerMovementConfigs>();
|
||||
_movementSettings = configs.TrashMazeMovement;
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update(); // Call base for movement and animation
|
||||
|
||||
// Update global shader properties for vision system
|
||||
UpdateShaderGlobals();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates global shader properties used by visibility shaders
|
||||
/// </summary>
|
||||
private void UpdateShaderGlobals()
|
||||
{
|
||||
Shader.SetGlobalVector(PlayerWorldPosID, transform.position);
|
||||
Shader.SetGlobalFloat(VisionRadiusID, visionRadius);
|
||||
}
|
||||
|
||||
internal override void OnManagedDestroy()
|
||||
{
|
||||
base.OnManagedDestroy();
|
||||
|
||||
if (Instance == this)
|
||||
{
|
||||
Instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set vision radius at runtime
|
||||
/// </summary>
|
||||
public void SetVisionRadius(float radius)
|
||||
{
|
||||
visionRadius = Mathf.Max(0.1f, radius);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
122
Assets/Scripts/Minigames/TrashMaze/Core/TrashMazeController.cs
Normal file
122
Assets/Scripts/Minigames/TrashMaze/Core/TrashMazeController.cs
Normal file
@@ -0,0 +1,122 @@
|
||||
using Core;
|
||||
using Core.Lifecycle;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Minigames.TrashMaze.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Main controller for the Trash Maze minigame.
|
||||
/// Initializes the vision system and manages game flow.
|
||||
/// </summary>
|
||||
public class TrashMazeController : ManagedBehaviour
|
||||
{
|
||||
public static TrashMazeController Instance { get; private set; }
|
||||
|
||||
[Header("Player")]
|
||||
[SerializeField] private PulverController pulverPrefab;
|
||||
[SerializeField] private Transform startPosition;
|
||||
|
||||
[Header("World Settings")]
|
||||
[SerializeField] private Vector2 worldSize = new Vector2(100f, 100f);
|
||||
[SerializeField] private Vector2 worldCenter = Vector2.zero;
|
||||
|
||||
[Header("Exit")]
|
||||
[SerializeField] private Transform exitPosition;
|
||||
|
||||
private PulverController _pulverInstance;
|
||||
private bool _mazeCompleted;
|
||||
|
||||
internal override void OnManagedAwake()
|
||||
{
|
||||
base.OnManagedAwake();
|
||||
|
||||
// Singleton pattern
|
||||
if (Instance != null && Instance != this)
|
||||
{
|
||||
Logging.Warning("[TrashMazeController] Duplicate instance detected. Destroying duplicate.");
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
Instance = this;
|
||||
}
|
||||
|
||||
internal override void OnManagedStart()
|
||||
{
|
||||
base.OnManagedStart();
|
||||
|
||||
Logging.Debug("[TrashMazeController] Initializing Trash Maze");
|
||||
|
||||
InitializeMaze();
|
||||
}
|
||||
|
||||
private void InitializeMaze()
|
||||
{
|
||||
// Set global shader properties for world bounds
|
||||
Shader.SetGlobalVector("_WorldSize", worldSize);
|
||||
Shader.SetGlobalVector("_WorldCenter", worldCenter);
|
||||
|
||||
// Spawn player
|
||||
SpawnPulver();
|
||||
|
||||
Logging.Debug("[TrashMazeController] Trash Maze initialized");
|
||||
}
|
||||
|
||||
private void SpawnPulver()
|
||||
{
|
||||
if (pulverPrefab == null)
|
||||
{
|
||||
Logging.Error("[TrashMazeController] Pulver prefab not assigned!");
|
||||
return;
|
||||
}
|
||||
|
||||
Vector3 spawnPosition = startPosition != null ? startPosition.position : Vector3.zero;
|
||||
_pulverInstance = Instantiate(pulverPrefab, spawnPosition, Quaternion.identity);
|
||||
|
||||
Logging.Debug($"[TrashMazeController] Pulver spawned at {spawnPosition}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when player reaches the maze exit
|
||||
/// </summary>
|
||||
public void OnExitReached()
|
||||
{
|
||||
if (_mazeCompleted)
|
||||
{
|
||||
Logging.Debug("[TrashMazeController] Maze already completed");
|
||||
return;
|
||||
}
|
||||
|
||||
_mazeCompleted = true;
|
||||
|
||||
Logging.Debug("[TrashMazeController] Maze completed! Player reached exit.");
|
||||
|
||||
// TODO: Trigger completion events
|
||||
// - Award booster packs collected
|
||||
// - Open gate for Trafalgar
|
||||
// - Switch control to Trafalgar
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when player collects a booster pack
|
||||
/// </summary>
|
||||
public void OnBoosterPackCollected()
|
||||
{
|
||||
Logging.Debug("[TrashMazeController] Booster pack collected");
|
||||
|
||||
// TODO: Integrate with card album system
|
||||
// CardAlbum.AddBoosterPack();
|
||||
}
|
||||
|
||||
internal override void OnManagedDestroy()
|
||||
{
|
||||
base.OnManagedDestroy();
|
||||
|
||||
if (Instance == this)
|
||||
{
|
||||
Instance = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
9
Assets/Scripts/Minigames/TrashMaze/Objects.meta
Normal file
9
Assets/Scripts/Minigames/TrashMaze/Objects.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
175
Assets/Scripts/Minigames/TrashMaze/Objects/RevealableObject.cs
Normal file
175
Assets/Scripts/Minigames/TrashMaze/Objects/RevealableObject.cs
Normal file
@@ -0,0 +1,175 @@
|
||||
using Core;
|
||||
using Minigames.TrashMaze.Core;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Minigames.TrashMaze.Objects
|
||||
{
|
||||
/// <summary>
|
||||
/// Component for objects that need reveal memory (obstacles, booster packs, treasures).
|
||||
/// Tracks if object has been revealed and updates material properties accordingly.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(SpriteRenderer))]
|
||||
public class RevealableObject : MonoBehaviour
|
||||
{
|
||||
[Header("Textures")]
|
||||
[SerializeField] private Sprite normalSprite;
|
||||
[SerializeField] private Sprite outlineSprite;
|
||||
|
||||
[Header("Object Type")]
|
||||
[SerializeField] private bool isBoosterPack = false;
|
||||
[SerializeField] private bool isExit = false;
|
||||
|
||||
private SpriteRenderer _spriteRenderer;
|
||||
private Material _instanceMaterial;
|
||||
private bool _hasBeenRevealed = false;
|
||||
private bool _isCollected = false;
|
||||
|
||||
// Material property IDs (cached for performance)
|
||||
private static readonly int IsRevealedID = Shader.PropertyToID("_IsRevealed");
|
||||
private static readonly int IsInVisionID = Shader.PropertyToID("_IsInVision");
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_spriteRenderer = GetComponent<SpriteRenderer>();
|
||||
|
||||
// Create instance material so each object has its own properties
|
||||
if (_spriteRenderer.material != null)
|
||||
{
|
||||
_instanceMaterial = new Material(_spriteRenderer.material);
|
||||
_spriteRenderer.material = _instanceMaterial;
|
||||
}
|
||||
else
|
||||
{
|
||||
Logging.Error($"[RevealableObject] No material assigned to {gameObject.name}");
|
||||
}
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// Initialize material properties
|
||||
if (_instanceMaterial != null)
|
||||
{
|
||||
_instanceMaterial.SetFloat(IsRevealedID, 0f);
|
||||
_instanceMaterial.SetFloat(IsInVisionID, 0f);
|
||||
|
||||
// Set textures if provided
|
||||
if (normalSprite != null)
|
||||
{
|
||||
_instanceMaterial.SetTexture("_MainTex", normalSprite.texture);
|
||||
}
|
||||
if (outlineSprite != null)
|
||||
{
|
||||
_instanceMaterial.SetTexture("_OutlineTex", outlineSprite.texture);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (_isCollected || _instanceMaterial == null) return;
|
||||
|
||||
// Calculate distance to player
|
||||
float distance = Vector2.Distance(transform.position, PulverController.PlayerPosition);
|
||||
bool isInRadius = distance < PulverController.VisionRadius;
|
||||
|
||||
// Update real-time vision flag
|
||||
_instanceMaterial.SetFloat(IsInVisionID, isInRadius ? 1f : 0f);
|
||||
|
||||
// Set revealed flag (once true, stays true)
|
||||
if (isInRadius && !_hasBeenRevealed)
|
||||
{
|
||||
_hasBeenRevealed = true;
|
||||
_instanceMaterial.SetFloat(IsRevealedID, 1f);
|
||||
|
||||
Logging.Debug($"[RevealableObject] {gameObject.name} revealed!");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTriggerEnter2D(Collider2D other)
|
||||
{
|
||||
// Check if player is interacting
|
||||
if (other.CompareTag("Player") && _hasBeenRevealed && !_isCollected)
|
||||
{
|
||||
HandleInteraction();
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleInteraction()
|
||||
{
|
||||
if (isBoosterPack)
|
||||
{
|
||||
CollectBoosterPack();
|
||||
}
|
||||
else if (isExit)
|
||||
{
|
||||
ActivateExit();
|
||||
}
|
||||
}
|
||||
|
||||
private void CollectBoosterPack()
|
||||
{
|
||||
_isCollected = true;
|
||||
|
||||
Logging.Debug($"[RevealableObject] Booster pack collected: {gameObject.name}");
|
||||
|
||||
// Notify controller
|
||||
if (TrashMazeController.Instance != null)
|
||||
{
|
||||
TrashMazeController.Instance.OnBoosterPackCollected();
|
||||
}
|
||||
|
||||
// Destroy object
|
||||
Destroy(gameObject);
|
||||
}
|
||||
|
||||
private void ActivateExit()
|
||||
{
|
||||
Logging.Debug($"[RevealableObject] Exit activated: {gameObject.name}");
|
||||
|
||||
// Notify controller
|
||||
if (TrashMazeController.Instance != null)
|
||||
{
|
||||
TrashMazeController.Instance.OnExitReached();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
// Clean up instance material
|
||||
if (_instanceMaterial != null)
|
||||
{
|
||||
Destroy(_instanceMaterial);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if object is currently visible to player
|
||||
/// </summary>
|
||||
public bool IsVisible()
|
||||
{
|
||||
float distance = Vector2.Distance(transform.position, PulverController.PlayerPosition);
|
||||
return distance < PulverController.VisionRadius;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if object has been revealed at any point
|
||||
/// </summary>
|
||||
public bool HasBeenRevealed()
|
||||
{
|
||||
return _hasBeenRevealed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Force reveal the object (for debugging or special cases)
|
||||
/// </summary>
|
||||
public void ForceReveal()
|
||||
{
|
||||
_hasBeenRevealed = true;
|
||||
if (_instanceMaterial != null)
|
||||
{
|
||||
_instanceMaterial.SetFloat(IsRevealedID, 1f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
||||
@@ -38,7 +38,7 @@ public class FollowerController : ManagedBehaviour
|
||||
public float manualMoveSmooth = 8f;
|
||||
|
||||
// Settings reference
|
||||
private IPlayerFollowerSettings _settings;
|
||||
private IFollowerSettings _settings;
|
||||
private IInteractionSettings _interactionSettings;
|
||||
|
||||
private GameObject _playerRef;
|
||||
@@ -123,7 +123,8 @@ public class FollowerController : ManagedBehaviour
|
||||
}
|
||||
|
||||
// Initialize settings references
|
||||
_settings = GameManager.GetSettingsObject<IPlayerFollowerSettings>();
|
||||
var configs = GameManager.GetSettingsObject<IPlayerMovementConfigs>();
|
||||
_settings = configs.FollowerMovement;
|
||||
_interactionSettings = GameManager.GetSettingsObject<IInteractionSettings>();
|
||||
}
|
||||
|
||||
@@ -295,7 +296,7 @@ public class FollowerController : ManagedBehaviour
|
||||
moveDir = _playerAIPath.velocity.normalized;
|
||||
_lastMoveDir = moveDir;
|
||||
}
|
||||
else if (_playerTouchController != null && _playerTouchController.isHolding && _playerTouchController.LastDirectMoveDir.sqrMagnitude > 0.01f)
|
||||
else if (_playerTouchController != null && _playerTouchController.IsHolding && _playerTouchController.LastDirectMoveDir.sqrMagnitude > 0.01f)
|
||||
{
|
||||
moveDir = _playerTouchController.LastDirectMoveDir;
|
||||
_lastMoveDir = moveDir;
|
||||
|
||||
Reference in New Issue
Block a user