First steps

This commit is contained in:
Michal Pikulski
2025-10-01 14:35:17 +02:00
parent a6c63af911
commit dac119fd7b
30 changed files with 806 additions and 9 deletions

View File

@@ -187,6 +187,11 @@ public class GameManager : MonoBehaviour
public LayerMask InteractableLayerMask => GetSettings<IInteractionSettings>()?.InteractableLayerMask ?? -1;
public GameObject BasePickupPrefab => GetSettings<IInteractionSettings>()?.BasePickupPrefab;
public GameObject LevelSwitchMenuPrefab => GetSettings<IInteractionSettings>()?.LevelSwitchMenuPrefab;
// PUZZLE SETTINGS
public float DefaultPuzzlePromptRange => GetSettings<IInteractionSettings>()?.DefaultPuzzlePromptRange ?? 3.0f;
public GameObject DefaultPuzzleIndicatorPrefab => GetSettings<IInteractionSettings>()?.DefaultPuzzleIndicatorPrefab;
/// <summary>
/// Returns the combination rule for two items, if any.

View File

@@ -22,6 +22,12 @@ namespace AppleHills.Core.Settings
[SerializeField] private GameObject basePickupPrefab;
[SerializeField] private GameObject levelSwitchMenuPrefab;
[Header("Puzzle Settings")]
[Tooltip("Default prefab for puzzle step indicators")]
[SerializeField] private GameObject defaultPuzzleIndicatorPrefab;
[Tooltip("Default range for puzzle prompts")]
[SerializeField] private float defaultPuzzlePromptRange = 3.0f;
[Header("Item Configuration")]
[SerializeField] private List<CombinationRule> combinationRules = new List<CombinationRule>();
[SerializeField] private List<SlotItemConfig> slotItemConfigs = new List<SlotItemConfig>();
@@ -35,6 +41,8 @@ namespace AppleHills.Core.Settings
public GameObject LevelSwitchMenuPrefab => levelSwitchMenuPrefab;
public List<CombinationRule> CombinationRules => combinationRules;
public List<SlotItemConfig> SlotItemConfigs => slotItemConfigs;
public GameObject DefaultPuzzleIndicatorPrefab => defaultPuzzleIndicatorPrefab;
public float DefaultPuzzlePromptRange => defaultPuzzlePromptRange;
public override void OnValidate()
{
@@ -43,6 +51,7 @@ namespace AppleHills.Core.Settings
playerStopDistance = Mathf.Max(0.1f, playerStopDistance);
playerStopDistanceDirectInteraction = Mathf.Max(0.1f, playerStopDistanceDirectInteraction);
followerPickupDelay = Mathf.Max(0f, followerPickupDelay);
defaultPuzzlePromptRange = Mathf.Max(0.1f, defaultPuzzlePromptRange);
}
}
}

View File

@@ -38,6 +38,10 @@ namespace AppleHills.Core.Settings
GameObject LevelSwitchMenuPrefab { get; }
List<CombinationRule> CombinationRules { get; }
List<SlotItemConfig> SlotItemConfigs { get; }
// Puzzle settings
GameObject DefaultPuzzleIndicatorPrefab { get; }
float DefaultPuzzlePromptRange { get; }
}
/// <summary>

View File

@@ -13,17 +13,20 @@ namespace AppleHills
// Static delegates that will be set by editor code
private static GetSettingsValueDelegate getPlayerStopDistanceProvider;
private static GetSettingsValueDelegate getPlayerStopDistanceDirectInteractionProvider;
private static GetSettingsValueDelegate getPuzzlePromptRangeProvider;
// Editor-only method to set up providers - will be called from editor code
public static void SetupEditorProviders(
GetSettingsValueDelegate playerStopDistanceProvider,
GetSettingsValueDelegate playerStopDistanceDirectInteractionProvider)
GetSettingsValueDelegate playerStopDistanceDirectInteractionProvider,
GetSettingsValueDelegate puzzlePromptRangeProvider)
{
#if UNITY_EDITOR
if (!Application.isPlaying)
{
getPlayerStopDistanceProvider = playerStopDistanceProvider;
getPlayerStopDistanceDirectInteractionProvider = playerStopDistanceDirectInteractionProvider;
getPuzzlePromptRangeProvider = puzzlePromptRangeProvider;
}
#endif
}
@@ -52,6 +55,18 @@ namespace AppleHills
return GameManager.Instance.PlayerStopDistanceDirectInteraction;
}
public static float GetPuzzlePromptRange()
{
#if UNITY_EDITOR
if (!Application.isPlaying && getPuzzlePromptRangeProvider != null)
{
return getPuzzlePromptRangeProvider();
}
#endif
return GameManager.Instance.DefaultPuzzlePromptRange;
}
// Add more methods as needed for other settings
}
}

View File

@@ -177,7 +177,7 @@ namespace Interactions
// the correct item we're looking for
var config = GameManager.Instance.GetSlotItemConfig(itemData);
var allowed = config?.allowedItems ?? new List<PickupItemData>();
if (PickupItemData.ListContainsEquivalent(allowed, itemToSlotData))
if (itemToSlotData != null && PickupItemData.ListContainsEquivalent(allowed, itemToSlotData))
{
if (itemToSlot != null)
{

View File

@@ -0,0 +1,31 @@
using System;
namespace PuzzleS
{
/// <summary>
/// Interface for proximity-based puzzle prompts that can show far/close indicators
/// based on player distance.
/// </summary>
public interface IPuzzlePrompt
{
/// <summary>
/// Called when the player enters the outer range of the prompt.
/// </summary>
void ShowFar();
/// <summary>
/// Called when the player enters the inner range of the prompt.
/// </summary>
void ShowClose();
/// <summary>
/// Called when the player exits the inner range of the prompt.
/// </summary>
void HideClose();
/// <summary>
/// Called when the player exits the outer range of the prompt.
/// </summary>
void HideFar();
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 84466e5dfeeb4908adf7f0f1f2b82531
timeCreated: 1759312472

View File

@@ -1,6 +1,8 @@
using Input;
using Interactions;
using UnityEngine;
using System;
using AppleHills.Core.Settings;
namespace PuzzleS
{
@@ -8,14 +10,27 @@ namespace PuzzleS
/// Manages the state and interactions for a single puzzle step, including unlock/lock logic and event handling.
/// </summary>
[RequireComponent(typeof(Interactable))]
public class ObjectiveStepBehaviour : MonoBehaviour
public class ObjectiveStepBehaviour : MonoBehaviour, IPuzzlePrompt
{
/// <summary>
/// The data object representing this puzzle step.
/// </summary>
public PuzzleStepSO stepData;
[Header("Indicator Settings")]
[SerializeField] private GameObject indicatorPrefab;
[SerializeField] private bool drawPromptRangeGizmo = true;
private Interactable _interactable;
private bool _isUnlocked = false;
private GameObject _spawnedIndicator;
private IPuzzlePrompt _indicator;
// Current proximity state tracked by PuzzleManager
private ProximityState _currentProximityState = ProximityState.Far;
// Enum for tracking proximity state (simplified to just Close and Far)
public enum ProximityState { Close, Far }
void Awake()
{
@@ -47,6 +62,103 @@ namespace PuzzleS
_interactable.interactionComplete.RemoveListener(OnInteractionComplete);
}
PuzzleManager.Instance?.UnregisterStepBehaviour(this);
// Clean up indicator
if (_spawnedIndicator != null)
{
Destroy(_spawnedIndicator);
_spawnedIndicator = null;
_indicator = null;
}
}
/// <summary>
/// Updates the proximity state from PuzzleManager and triggers appropriate methods.
/// </summary>
/// <param name="newState">The new proximity state.</param>
public void UpdateProximityState(ProximityState newState)
{
if (_currentProximityState == newState) return;
// Determine state changes and call appropriate methods
if (newState == ProximityState.Close)
{
// Transitioning from Far to Close
ShowClose();
}
else // newState == ProximityState.Far
{
// Transitioning from Close to Far
HideClose();
}
_currentProximityState = newState;
}
// IPuzzlePrompt interface implementation - delegates to indicator if available
/// <summary>
/// Called when player enters the far range
/// </summary>
public virtual void ShowFar()
{
// Delegate to indicator if available
if (_indicator != null)
{
_indicator.ShowFar();
return;
}
// Default fallback behavior
Debug.Log($"[Puzzles] Player entered far range of {stepData?.stepId} on {gameObject.name}");
}
/// <summary>
/// Called when player enters the close range
/// </summary>
public virtual void ShowClose()
{
// Delegate to indicator if available
if (_indicator != null)
{
_indicator.ShowClose();
return;
}
// Default fallback behavior
Debug.Log($"[Puzzles] Player entered close range of {stepData?.stepId} on {gameObject.name}");
}
/// <summary>
/// Called when player exits the close range
/// </summary>
public virtual void HideClose()
{
// Delegate to indicator if available
if (_indicator != null)
{
_indicator.HideClose();
return;
}
// Default fallback behavior
Debug.Log($"[Puzzles] Player exited close range of {stepData?.stepId} on {gameObject.name}");
}
/// <summary>
/// Called when player exits the far range
/// </summary>
public virtual void HideFar()
{
// Delegate to indicator if available
if (_indicator != null)
{
_indicator.HideFar();
return;
}
// Default fallback behavior
Debug.Log($"[Puzzles] Player exited far range of {stepData?.stepId} on {gameObject.name}");
}
/// <summary>
@@ -56,7 +168,54 @@ namespace PuzzleS
{
_isUnlocked = true;
Debug.Log($"[Puzzles] Step unlocked: {stepData?.stepId} on {gameObject.name}");
// Optionally, show visual feedback for unlocked state
// Show indicator if enabled in settings
if (stepData?.ShowIndicator == true && indicatorPrefab != null && _spawnedIndicator == null)
{
_spawnedIndicator = Instantiate(indicatorPrefab, transform);
// Try to get the IPuzzlePrompt component from the spawned indicator
_indicator = _spawnedIndicator.GetComponent<IPuzzlePrompt>();
if (_indicator == null)
{
// Try to find it in children if not on the root
_indicator = _spawnedIndicator.GetComponentInChildren<IPuzzlePrompt>();
}
if (_indicator == null)
{
Debug.LogWarning($"[Puzzles] Indicator prefab for {stepData?.stepId} does not implement IPuzzlePrompt");
}
else
{
// Immediately set the correct state based on current player distance
Transform playerTransform = GameObject.FindGameObjectWithTag("Player")?.transform;
if (playerTransform != null)
{
float distance = Vector3.Distance(transform.position, playerTransform.position);
float promptRange = AppleHills.SettingsAccess.GetPuzzlePromptRange();
if (distance <= promptRange)
{
// Player is in close range
_currentProximityState = ProximityState.Close;
_indicator.ShowClose();
}
else
{
// Player is in far range
_currentProximityState = ProximityState.Far;
_indicator.ShowFar();
}
}
else
{
// Default to far if player not found
_currentProximityState = ProximityState.Far;
_indicator.ShowFar();
}
}
}
}
/// <summary>
@@ -66,7 +225,14 @@ namespace PuzzleS
{
_isUnlocked = false;
Debug.Log($"[Puzzles] Step locked: {stepData?.stepId} on {gameObject.name}");
// Optionally, show visual feedback for locked state
// Hide indicator
if (_spawnedIndicator != null)
{
Destroy(_spawnedIndicator);
_spawnedIndicator = null;
_indicator = null;
}
}
/// <summary>
@@ -98,5 +264,20 @@ namespace PuzzleS
PuzzleManager.Instance?.MarkPuzzleStepCompleted(stepData);
}
}
/// <summary>
/// Visualizes the puzzle prompt ranges in the editor.
/// </summary>
private void OnDrawGizmos()
{
if (!drawPromptRangeGizmo) return;
// Use the global puzzle prompt range from settings
float promptRange = AppleHills.SettingsAccess.GetPuzzlePromptRange();
// Draw threshold circle
Gizmos.color = Color.cyan;
Gizmos.DrawWireSphere(transform.position, promptRange / 2f);
}
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
@@ -14,6 +15,12 @@ namespace PuzzleS
private static PuzzleManager _instance;
private static bool _isQuitting;
[SerializeField] private float proximityCheckInterval = 0.02f;
// Reference to player transform for proximity calculations
private Transform _playerTransform;
private Coroutine _proximityCheckCoroutine;
/// <summary>
/// Singleton instance of the PuzzleManager.
/// </summary>
@@ -53,9 +60,19 @@ namespace PuzzleS
SceneManager.sceneLoaded += OnSceneLoaded;
}
void Start()
{
// Find player transform
_playerTransform = GameObject.FindGameObjectWithTag("Player")?.transform;
// Start proximity check coroutine
StartProximityChecks();
}
void OnDestroy()
{
SceneManager.sceneLoaded -= OnSceneLoaded;
StopProximityChecks();
}
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
@@ -66,6 +83,69 @@ namespace PuzzleS
_runtimeDependencies.Clear();
BuildRuntimeDependencies();
UnlockInitialSteps();
// Find player transform again in case it changed with scene load
_playerTransform = GameObject.FindGameObjectWithTag("Player")?.transform;
// Restart proximity checks
StartProximityChecks();
}
/// <summary>
/// Start the proximity check coroutine.
/// </summary>
private void StartProximityChecks()
{
StopProximityChecks();
_proximityCheckCoroutine = StartCoroutine(CheckProximityRoutine());
}
/// <summary>
/// Stop the proximity check coroutine.
/// </summary>
private void StopProximityChecks()
{
if (_proximityCheckCoroutine != null)
{
StopCoroutine(_proximityCheckCoroutine);
_proximityCheckCoroutine = null;
}
}
/// <summary>
/// Coroutine that periodically checks player proximity to all puzzle steps.
/// </summary>
private IEnumerator CheckProximityRoutine()
{
WaitForSeconds wait = new WaitForSeconds(proximityCheckInterval);
while (true)
{
if (_playerTransform != null)
{
// Get the proximity threshold from settings (half of the prompt range)
float proximityThreshold = GameManager.Instance.DefaultPuzzlePromptRange;
// Check distance to each step behavior
foreach (var kvp in _stepBehaviours)
{
if (kvp.Value == null) continue;
float distance = Vector3.Distance(_playerTransform.position, kvp.Value.transform.position);
// Determine the proximity state - only Close or Far now
ObjectiveStepBehaviour.ProximityState state =
(distance <= proximityThreshold)
? ObjectiveStepBehaviour.ProximityState.Close
: ObjectiveStepBehaviour.ProximityState.Far;
// Update the step's proximity state
kvp.Value.UpdateProximityState(state);
}
}
yield return wait;
}
}
/// <summary>

View File

@@ -29,4 +29,19 @@ public class PuzzleStepSO : ScriptableObject
/// </summary>
[Header("Unlocks")]
public List<PuzzleStepSO> unlocks = new List<PuzzleStepSO>();
[Header("Interaction Settings")]
[Tooltip("Whether to show an indicator when this step is unlocked")]
[SerializeField] private bool showIndicator = false;
/// <summary>
/// Whether to show an indicator when this step is unlocked.
/// </summary>
public bool ShowIndicator => showIndicator;
/// <summary>
/// Gets or sets whether to show an indicator.
/// </summary>
public bool GetShowIndicator() => showIndicator;
public void SetShowIndicator(bool value) => showIndicator = value;
}

View File

@@ -0,0 +1,36 @@
using UnityEngine;
public class TestIndicator : MonoBehaviour, PuzzleS.IPuzzlePrompt
{
// Start is called once before the first execution of Update after the MonoBehaviour is created
void Start()
{
}
// Update is called once per frame
void Update()
{
}
public void ShowFar()
{
gameObject.SetActive(true);
}
public void ShowClose()
{
gameObject.transform.localScale = new Vector3(-0.2f, 0.2f, 0.2f);
}
public void HideClose()
{
gameObject.transform.localScale = new Vector3(-0.1f, 0.1f, 0.1f);
}
public void HideFar()
{
gameObject.SetActive(false);
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 7a691a95f74c07245b64469dda594bfb