Merge branch origin/codex/add-player-interactions-and-critter-feeding into main

This commit is contained in:
Alexa Amundson
2025-11-28 23:18:44 -06:00
3 changed files with 194 additions and 0 deletions

View File

@@ -0,0 +1,24 @@
using UnityEngine;
namespace BlackRoad.Worldbuilder.Interaction
{
/// <summary>
/// Base component for objects that can be interacted with by the player.
/// </summary>
public abstract class Interactable : MonoBehaviour
{
[Tooltip("Display name shown in UI when player looks at this.")]
[SerializeField] private string displayName = "Object";
[Tooltip("Optional interaction verb (e.g., 'Feed', 'Open', 'Talk').")]
[SerializeField] private string verb = "Use";
public string DisplayName => displayName;
public string Verb => verb;
/// <summary>
/// Called when the player attempts to interact with this object.
/// </summary>
public abstract void Interact(GameObject interactor);
}
}

View File

@@ -0,0 +1,71 @@
using UnityEngine;
namespace BlackRoad.Worldbuilder.Interaction
{
/// <summary>
/// Raycasts from the center of the player's view to find Interactable objects.
/// Shows a simple on-screen prompt and calls Interact() on key press.
/// </summary>
public class PlayerInteractor : MonoBehaviour
{
[Header("Refs")]
[SerializeField] private Camera playerCamera;
[Header("Settings")]
[SerializeField] private float interactDistance = 4f;
[SerializeField] private KeyCode interactKey = KeyCode.E;
[Header("UI")]
[SerializeField] private bool drawPrompt = true;
private Interactable _current;
private void Awake()
{
if (playerCamera == null)
playerCamera = Camera.main;
}
private void Update()
{
UpdateCurrentTarget();
if (_current != null && Input.GetKeyDown(interactKey))
{
_current.Interact(gameObject);
}
}
private void UpdateCurrentTarget()
{
_current = null;
if (playerCamera == null) return;
Ray ray = playerCamera.ViewportPointToRay(new Vector3(0.5f, 0.5f, 0f));
if (Physics.Raycast(ray, out RaycastHit hit, interactDistance))
{
_current = hit.collider.GetComponentInParent<Interactable>();
}
}
private void OnGUI()
{
if (!drawPrompt || _current == null) return;
string label = $"{_current.Verb} {_current.DisplayName} [{interactKey}]";
GUIStyle style = GUI.skin.label;
style.alignment = TextAnchor.LowerCenter;
style.fontSize = 16;
Rect rect = new Rect(
0,
Screen.height - 40,
Screen.width,
30
);
GUI.Label(rect, label, style);
}
}
}

View File

@@ -0,0 +1,99 @@
using UnityEngine;
using BlackRoad.Worldbuilder.Interaction;
namespace BlackRoad.Worldbuilder.Life
{
/// <summary>
/// Allows the player to feed and "pet" critters.
/// Feeding reduces hunger via CritterNeeds.
/// Petting increases a simple trust value.
/// </summary>
public class CritterInteraction : Interactable
{
[Header("Needs & Behaviour")]
[SerializeField] private CritterNeeds needs;
[SerializeField] private CritterAgent agent;
[Header("Interaction Effects")]
[Tooltip("How much hunger to reduce per feeding (0..1).")]
[SerializeField] private float feedAmount = 0.25f;
[Tooltip("How much trust is gained per pet.")]
[SerializeField] private float trustIncrease = 0.1f;
[Header("Trust")]
[Range(0f, 1f)]
[SerializeField] private float trust = 0f;
public float Trust => trust;
private void Awake()
{
if (needs == null)
needs = GetComponent<CritterNeeds>();
if (agent == null)
agent = GetComponent<CritterAgent>();
}
public override void Interact(GameObject interactor)
{
// Simple rule:
// - If critter is hungry -> feeding action
// - Else -> pet action
if (needs != null && needs.IsHungry)
{
Feed();
}
else
{
Pet();
}
}
private void Feed()
{
if (needs == null) return;
// Feed: lower hunger instantly by some amount.
needs.SetHunger(Mathf.Clamp01(needs.Hunger - feedAmount));
trust = Mathf.Clamp01(trust + trustIncrease * 0.5f);
// Small "happy" animation cue (bob up slightly).
StartCoroutine(BobAnimation(Color.green));
}
private void Pet()
{
trust = Mathf.Clamp01(trust + trustIncrease);
// Slight bob to show reaction.
StartCoroutine(BobAnimation(Color.cyan));
}
private System.Collections.IEnumerator BobAnimation(Color color)
{
Renderer rend = GetComponentInChildren<Renderer>();
Color original = rend != null ? rend.material.color : Color.white;
float t = 0f;
Vector3 basePos = transform.position;
while (t < 0.4f)
{
t += Time.deltaTime;
float bob = Mathf.Sin(t * Mathf.PI * 4f) * 0.05f;
transform.position = basePos + Vector3.up * bob;
if (rend != null)
{
rend.material.color = Color.Lerp(original, color, t / 0.4f);
}
yield return null;
}
transform.position = basePos;
if (rend != null)
rend.material.color = original;
}
}
}