feat: Implement NavMesh pathfinding and Deep Social visuals
- Phase 20-F: NavMesh Integration - Added 'com.unity.ai.navigation' package - Implemented Runtime NavMesh Baking in EnvironmentManager - Added NavMeshObstacle to environmental assets - Updated AgentVisual to use NavMeshAgent for movement - Implemented 'Instinctive Avoidance' via target offsetting - Phase 21: Social Interaction & Expressions - Added procedural Dance and Wave animations in AgentAnimator - Implemented 'Dance Party' triggering logic in engine.py and AgentVisual - Added social relationship syncing (Backend -> Frontend) - Implemented proximity-based social greetings (Heart emote + Wave) - Updated Models.cs to support relationship data parsing
This commit is contained in:
@@ -202,9 +202,31 @@ class GameEngine:
|
|||||||
"""Broadcast all agents' current status."""
|
"""Broadcast all agents' current status."""
|
||||||
with get_db_session() as db:
|
with get_db_session() as db:
|
||||||
agents = db.query(Agent).all()
|
agents = db.query(Agent).all()
|
||||||
agents_data = [agent.to_dict() for agent in agents]
|
agents_data = []
|
||||||
|
for agent in agents:
|
||||||
|
data = agent.to_dict()
|
||||||
|
# Phase 21-B: Inject relationships
|
||||||
|
data["relationships"] = self._get_agent_relationships(db, agent.id)
|
||||||
|
agents_data.append(data)
|
||||||
await self._broadcast_event(EventType.AGENTS_UPDATE, {"agents": agents_data})
|
await self._broadcast_event(EventType.AGENTS_UPDATE, {"agents": agents_data})
|
||||||
|
|
||||||
|
def _get_agent_relationships(self, db, agent_id: int) -> list:
|
||||||
|
"""Fetch significant relationships for an agent."""
|
||||||
|
# Phase 21-B: Only send non-stranger relationships to save bandwidth
|
||||||
|
rels = db.query(AgentRelationship).filter(
|
||||||
|
AgentRelationship.agent_from_id == agent_id,
|
||||||
|
AgentRelationship.relationship_type != "stranger"
|
||||||
|
).all()
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for r in rels:
|
||||||
|
results.append({
|
||||||
|
"target_id": r.agent_to_id,
|
||||||
|
"type": r.relationship_type,
|
||||||
|
"affection": r.affection
|
||||||
|
})
|
||||||
|
return results
|
||||||
|
|
||||||
async def _broadcast_world_status(self) -> None:
|
async def _broadcast_world_status(self) -> None:
|
||||||
"""Broadcast world state."""
|
"""Broadcast world state."""
|
||||||
with get_db_session() as db:
|
with get_db_session() as db:
|
||||||
@@ -719,11 +741,31 @@ class GameEngine:
|
|||||||
target_name = friend.name
|
target_name = friend.name
|
||||||
should_update = True
|
should_update = True
|
||||||
|
|
||||||
# 3. Boredom / Wandering
|
# Phase 21: Social Interaction (Group Dance)
|
||||||
elif agent.current_action == "Idle" or agent.current_action is None:
|
# If Happy (>80) and near others, chance to start dancing
|
||||||
|
elif agent.mood > 80 and agent.current_action != "Dance":
|
||||||
|
# Check for nearby agents (same location)
|
||||||
|
nearby_count = 0
|
||||||
|
for other in agents:
|
||||||
|
if other.id != agent.id and other.status == "Alive" and other.location == agent.location:
|
||||||
|
nearby_count += 1
|
||||||
|
|
||||||
|
# Dance Party Trigger! (Need at least 1 friend, 10% chance)
|
||||||
|
if nearby_count >= 1 and random.random() < 0.10:
|
||||||
|
new_action = "Dance"
|
||||||
|
# Keep location same
|
||||||
|
new_location = agent.location
|
||||||
|
should_update = True
|
||||||
|
|
||||||
|
# 2. Idle Behavior (Default)
|
||||||
|
elif agent.current_action not in ["Sleep", "Chat", "Dance"]:
|
||||||
|
# Random chance to move nearby or chat
|
||||||
if random.random() < 0.3:
|
if random.random() < 0.3:
|
||||||
new_action = "Wander"
|
new_action = "Wander"
|
||||||
new_location = "nearby"
|
new_location = "nearby" # Will be randomized in Unity/GameManager mapping
|
||||||
|
should_update = True
|
||||||
|
elif random.random() < 0.1:
|
||||||
|
new_action = "Idle"
|
||||||
should_update = True
|
should_update = True
|
||||||
|
|
||||||
# 4. Finish Tasks (Simulation)
|
# 4. Finish Tasks (Simulation)
|
||||||
|
|||||||
@@ -212,7 +212,7 @@ class LLMService:
|
|||||||
f"Personality: {agent.personality}. "
|
f"Personality: {agent.personality}. "
|
||||||
f"Current Status: HP={agent.hp}, Energy={agent.energy}. "
|
f"Current Status: HP={agent.hp}, Energy={agent.energy}. "
|
||||||
f"Shelter Status: {'Under shelter (safe from weather)' if agent.is_sheltered else 'Exposed (vulnerable to weather)'}. "
|
f"Shelter Status: {'Under shelter (safe from weather)' if agent.is_sheltered else 'Exposed (vulnerable to weather)'}. "
|
||||||
f"You live on a survival island. "
|
f"You are a land creature on a survival island. You have a natural instinct to stay on the dry sand and avoid the deep ocean. "
|
||||||
f"Relevant Memories:\n{memory_context}\n"
|
f"Relevant Memories:\n{memory_context}\n"
|
||||||
f"React to the following event briefly (under 20 words). "
|
f"React to the following event briefly (under 20 words). "
|
||||||
f"Respond in first person, as if speaking out loud."
|
f"Respond in first person, as if speaking out loud."
|
||||||
@@ -278,7 +278,7 @@ class LLMService:
|
|||||||
f"Personality: {agent.personality}. "
|
f"Personality: {agent.personality}. "
|
||||||
f"Current Status: HP={agent.hp}, Energy={agent.energy}. "
|
f"Current Status: HP={agent.hp}, Energy={agent.energy}. "
|
||||||
f"Shelter Status: {'Under shelter (protected)' if agent.is_sheltered else 'Exposed to elements'}. "
|
f"Shelter Status: {'Under shelter (protected)' if agent.is_sheltered else 'Exposed to elements'}. "
|
||||||
f"You are stranded on a survival island. "
|
f"You are a land creature stranded on a survival island beach. You feel safer on dry land than near the waves. "
|
||||||
f"It is currently {time_of_day} and the weather is {weather}. "
|
f"It is currently {time_of_day} and the weather is {weather}. "
|
||||||
f"Say something brief (under 15 words) about your situation or thoughts. "
|
f"Say something brief (under 15 words) about your situation or thoughts. "
|
||||||
f"Speak naturally, as if talking to yourself or nearby survivors."
|
f"Speak naturally, as if talking to yourself or nearby survivors."
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
using System.Collections;
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
using UnityEngine.AI;
|
||||||
using UnityEngine.UI;
|
using UnityEngine.UI;
|
||||||
using TMPro;
|
using TMPro;
|
||||||
using TheIsland.Models;
|
using TheIsland.Models;
|
||||||
using TheIsland.Network;
|
using TheIsland.Network;
|
||||||
|
using TheIsland.Core; // Added for VFXManager
|
||||||
|
|
||||||
namespace TheIsland.Visual
|
namespace TheIsland.Visual
|
||||||
{
|
{
|
||||||
@@ -65,6 +68,7 @@ namespace TheIsland.Visual
|
|||||||
private Billboard _uiBillboard;
|
private Billboard _uiBillboard;
|
||||||
private Camera _mainCamera;
|
private Camera _mainCamera;
|
||||||
private AgentAnimator _animator;
|
private AgentAnimator _animator;
|
||||||
|
private NavMeshAgent _navAgent; // Added NavMeshAgent
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region State
|
#region State
|
||||||
@@ -86,6 +90,9 @@ namespace TheIsland.Visual
|
|||||||
private GameObject _shadowObj;
|
private GameObject _shadowObj;
|
||||||
private SpriteRenderer _shadowRenderer;
|
private SpriteRenderer _shadowRenderer;
|
||||||
private float _footstepTimer;
|
private float _footstepTimer;
|
||||||
|
private float _lastEmoteTime;
|
||||||
|
private float _lastMoveTime; // Added for NavMesh movement
|
||||||
|
private float _lastFootstepTime; // Added for NavMesh movement
|
||||||
|
|
||||||
// UI Smoothing (Phase 19)
|
// UI Smoothing (Phase 19)
|
||||||
private float _currentHpPercent;
|
private float _currentHpPercent;
|
||||||
@@ -94,6 +101,11 @@ namespace TheIsland.Visual
|
|||||||
private float _targetHpPercent;
|
private float _targetHpPercent;
|
||||||
private float _targetEnergyPercent;
|
private float _targetEnergyPercent;
|
||||||
private float _targetMoodPercent;
|
private float _targetMoodPercent;
|
||||||
|
|
||||||
|
// Phase 21-B: Social Visuals
|
||||||
|
private float _socialCheckTimer;
|
||||||
|
private Dictionary<int, RelationshipData> _relationships = new Dictionary<int, RelationshipData>();
|
||||||
|
private Dictionary<int, float> _lastGreetingTimes = new Dictionary<int, float>(); // Cooldown per agent
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Properties
|
#region Properties
|
||||||
@@ -112,6 +124,19 @@ namespace TheIsland.Visual
|
|||||||
_animator = GetComponent<AgentAnimator>();
|
_animator = GetComponent<AgentAnimator>();
|
||||||
if (_animator == null) _animator = gameObject.AddComponent<AgentAnimator>();
|
if (_animator == null) _animator = gameObject.AddComponent<AgentAnimator>();
|
||||||
|
|
||||||
|
// Phase 20-F: NavMeshAgent
|
||||||
|
_navAgent = GetComponent<NavMeshAgent>();
|
||||||
|
if (_navAgent == null) _navAgent = gameObject.AddComponent<NavMeshAgent>();
|
||||||
|
|
||||||
|
_navAgent.speed = _moveSpeed;
|
||||||
|
_navAgent.acceleration = 12f;
|
||||||
|
_navAgent.angularSpeed = 0f; // 2D Sprite, no rotation
|
||||||
|
_navAgent.radius = 0.3f; // Small footprint
|
||||||
|
_navAgent.height = 1.5f;
|
||||||
|
_navAgent.updateRotation = false;
|
||||||
|
_navAgent.updateUpAxis = true; // Use 3D physics (X-Z plane)
|
||||||
|
_navAgent.obstacleAvoidanceType = ObstacleAvoidanceType.HighQualityObstacleAvoidance;
|
||||||
|
|
||||||
CreateVisuals();
|
CreateVisuals();
|
||||||
CreateShadow();
|
CreateShadow();
|
||||||
_lastPosition = transform.position;
|
_lastPosition = transform.position;
|
||||||
@@ -149,35 +174,70 @@ namespace TheIsland.Visual
|
|||||||
|
|
||||||
private void Update()
|
private void Update()
|
||||||
{
|
{
|
||||||
if (!IsAlive) return;
|
if (!IsAlive)
|
||||||
|
{
|
||||||
|
if (_navAgent.enabled) _navAgent.isStopped = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Phase 19-D: Apply soft-repulsion to prevent crowding
|
// Phase 21: Handle Dance/Action Disable
|
||||||
Vector3 repulsion = CalculateRepulsion();
|
bool isDancing = (_currentData != null && _currentData.current_action == "Dance");
|
||||||
|
if (_animator != null) _animator.SetDancing(isDancing);
|
||||||
|
|
||||||
|
if (isDancing)
|
||||||
|
{
|
||||||
|
if (_navAgent.enabled) _navAgent.isStopped = true;
|
||||||
|
|
||||||
|
if (Time.time - _lastEmoteTime > 2f)
|
||||||
|
{
|
||||||
|
ShowEmotion("music");
|
||||||
|
_lastEmoteTime = Time.time;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Handle Movement
|
if (_navAgent.enabled) _navAgent.isStopped = false;
|
||||||
|
|
||||||
|
// Handle Movement via NavMesh
|
||||||
if (_isMoving)
|
if (_isMoving)
|
||||||
{
|
{
|
||||||
// Simple steering toward target
|
// Phase 20-E: Apply soft constraints to target before setting destination
|
||||||
Vector3 moveDir = (_targetPosition - transform.position).normalized;
|
// We offset the target based on shoreline repulsion, rather than applying force to velocity
|
||||||
Vector3 finalVelocity = (moveDir * _moveSpeed) + repulsion;
|
Vector3 instinctOffset = CalculateInstinctOffset(_targetPosition);
|
||||||
|
Vector3 safeTarget = _targetPosition + instinctOffset;
|
||||||
transform.position += finalVelocity * Time.deltaTime;
|
|
||||||
|
_navAgent.SetDestination(safeTarget);
|
||||||
|
|
||||||
|
// Sync Animator with NavAgent velocity
|
||||||
|
Vector3 vel = _navAgent.velocity;
|
||||||
|
|
||||||
|
// Manual flipping based on velocity X
|
||||||
|
if (Mathf.Abs(vel.x) > 0.1f)
|
||||||
|
{
|
||||||
|
bool flip = vel.x < 0;
|
||||||
|
if (_spriteRenderer.flipX != flip) _spriteRenderer.flipX = flip;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_animator != null) _animator.SetMovement(vel);
|
||||||
|
|
||||||
// Flip sprite based on direction
|
if (vel.sqrMagnitude > 0.1f)
|
||||||
if (_spriteRenderer != null && Mathf.Abs(moveDir.x) > 0.01f)
|
{
|
||||||
{
|
_lastMoveTime = Time.time;
|
||||||
_spriteRenderer.flipX = moveDir.x < 0;
|
if (Time.time - _lastFootstepTime > 0.3f)
|
||||||
}
|
{
|
||||||
|
VFXManager.Instance.SpawnFootstepDust(transform.position);
|
||||||
if (Vector3.Distance(transform.position, _targetPosition) < 0.1f)
|
_lastFootstepTime = Time.time;
|
||||||
{
|
}
|
||||||
_isMoving = false;
|
}
|
||||||
}
|
else
|
||||||
|
{
|
||||||
|
// Agent might be moving but stuck or thinking
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (repulsion.sqrMagnitude > 0.001f)
|
else
|
||||||
{
|
{
|
||||||
// Push away even when idle
|
if (_navAgent.enabled && _navAgent.isOnNavMesh) _navAgent.ResetPath();
|
||||||
transform.position += repulsion * Time.deltaTime;
|
if (_animator != null) _animator.SetMovement(Vector3.zero);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 19-D: Dynamic Z-Sorting
|
// Phase 19-D: Dynamic Z-Sorting
|
||||||
@@ -197,12 +257,16 @@ namespace TheIsland.Visual
|
|||||||
_lastPosition = transform.position;
|
_lastPosition = transform.position;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phase 19-E: Social Orientation (Interaction Facing)
|
||||||
// Phase 19-E: Social Orientation (Interaction Facing)
|
// Phase 19-E: Social Orientation (Interaction Facing)
|
||||||
if (!_isMoving)
|
if (!_isMoving)
|
||||||
{
|
{
|
||||||
FaceInteractionTarget();
|
FaceInteractionTarget();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phase 21-B: Social Visuals (Heart/Wave)
|
||||||
|
CheckSocialInteractions();
|
||||||
|
|
||||||
// Phase 19-F: AAA Grounding (Shadow & Footsteps)
|
// Phase 19-F: AAA Grounding (Shadow & Footsteps)
|
||||||
UpdateGrounding();
|
UpdateGrounding();
|
||||||
|
|
||||||
@@ -330,6 +394,21 @@ namespace TheIsland.Visual
|
|||||||
for (int y = 10; y < 24; y++) tex.SetPixel(16, y, iconColor);
|
for (int y = 10; y < 24; y++) tex.SetPixel(16, y, iconColor);
|
||||||
tex.SetPixel(16, 8, iconColor);
|
tex.SetPixel(16, 8, iconColor);
|
||||||
}
|
}
|
||||||
|
// Phase 21-B: Heart emote
|
||||||
|
else if (type == "heart") {
|
||||||
|
Color heartColor = new Color(1f, 0.4f, 0.5f);
|
||||||
|
for (int x=0; x<size; x++) {
|
||||||
|
for (int y=0; y<size; y++) {
|
||||||
|
// Simple implicit heart shape equation: (x^2+y^2-1)^3 - x^2*y^3 <= 0
|
||||||
|
// scaled to fit 32x32
|
||||||
|
float u = (x - 16) / 10f;
|
||||||
|
float v = (y - 14) / 10f;
|
||||||
|
if ((u*u + v*v - 1)*(u*u + v*v - 1)*(u*u + v*v - 1) - u*u*v*v*v <= 0) {
|
||||||
|
tex.SetPixel(x, y, heartColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tex.Apply();
|
tex.Apply();
|
||||||
return Sprite.Create(tex, new Rect(0, 0, size, size), new Vector2(0.5f, 0.5f));
|
return Sprite.Create(tex, new Rect(0, 0, size, size), new Vector2(0.5f, 0.5f));
|
||||||
@@ -382,6 +461,22 @@ namespace TheIsland.Visual
|
|||||||
return force;
|
return force;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determine instinctual offset for a target position.
|
||||||
|
/// If target is too close to water, aim slightly inland.
|
||||||
|
/// </summary>
|
||||||
|
private Vector3 CalculateInstinctOffset(Vector3 intendedTarget)
|
||||||
|
{
|
||||||
|
float fearThreshold = 5.0f; // Start getting anxious
|
||||||
|
if (intendedTarget.z > fearThreshold)
|
||||||
|
{
|
||||||
|
// Push target back to safety
|
||||||
|
float overshoot = intendedTarget.z - fearThreshold;
|
||||||
|
return new Vector3(0, 0, -overshoot * 1.5f);
|
||||||
|
}
|
||||||
|
return Vector3.zero;
|
||||||
|
}
|
||||||
|
|
||||||
private void UpdateSmoothBars()
|
private void UpdateSmoothBars()
|
||||||
{
|
{
|
||||||
float lerpSpeed = 5f * Time.deltaTime;
|
float lerpSpeed = 5f * Time.deltaTime;
|
||||||
@@ -491,6 +586,75 @@ namespace TheIsland.Visual
|
|||||||
|
|
||||||
UpdateStats(data);
|
UpdateStats(data);
|
||||||
Debug.Log($"[AgentVisual] Initialized: {data.name}");
|
Debug.Log($"[AgentVisual] Initialized: {data.name}");
|
||||||
|
_relationships.Clear();
|
||||||
|
if (data.relationships != null)
|
||||||
|
{
|
||||||
|
foreach (var r in data.relationships)
|
||||||
|
{
|
||||||
|
_relationships[r.target_id] = r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CheckSocialInteractions()
|
||||||
|
{
|
||||||
|
_socialCheckTimer += Time.deltaTime;
|
||||||
|
if (_socialCheckTimer < 1.0f) return; // Check every 1s
|
||||||
|
_socialCheckTimer = 0;
|
||||||
|
|
||||||
|
if (GameManager.Instance == null) return;
|
||||||
|
|
||||||
|
foreach (var kvp in GameManager.Instance.AllAgentVisuals)
|
||||||
|
{
|
||||||
|
int otherId = kvp.Key;
|
||||||
|
AgentVisual other = kvp.Value;
|
||||||
|
|
||||||
|
if (otherId == _agentId || !other.IsAlive) continue;
|
||||||
|
|
||||||
|
float dist = Vector3.Distance(transform.position, other.transform.position);
|
||||||
|
|
||||||
|
// If close enough (< 2.5m)
|
||||||
|
if (dist < 2.5f)
|
||||||
|
{
|
||||||
|
// Check if we have a special relationship
|
||||||
|
if (_relationships.TryGetValue(otherId, out RelationshipData rel))
|
||||||
|
{
|
||||||
|
// Logic for Close Friend / Friend
|
||||||
|
if (rel.type == "close_friend" || rel.type == "friend")
|
||||||
|
{
|
||||||
|
// Check greeting cooldown (e.g., once every 60s per friend)
|
||||||
|
if (!_lastGreetingTimes.ContainsKey(otherId) || Time.time - _lastGreetingTimes[otherId] > 60f)
|
||||||
|
{
|
||||||
|
// Trigger Greet
|
||||||
|
TriggerSocialGreet(otherId, rel.type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TriggerSocialGreet(int targetId, string type)
|
||||||
|
{
|
||||||
|
_lastGreetingTimes[targetId] = Time.time;
|
||||||
|
|
||||||
|
// Visuals
|
||||||
|
string emote = (type == "close_friend") ? "heart" : "music"; // Heart for close friends, music note/smile for friends
|
||||||
|
ShowEmotion(emote);
|
||||||
|
|
||||||
|
// Animation
|
||||||
|
if (_animator != null)
|
||||||
|
{
|
||||||
|
_animator.SetWaving(true);
|
||||||
|
// Stop waving after 2s
|
||||||
|
StartCoroutine(StopWavingAfterDelay(2.0f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerator StopWavingAfterDelay(float delay)
|
||||||
|
{
|
||||||
|
yield return new WaitForSeconds(delay);
|
||||||
|
if (_animator != null) _animator.SetWaving(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void TryLoadPremiumSprite(int id)
|
private void TryLoadPremiumSprite(int id)
|
||||||
@@ -1280,7 +1444,24 @@ namespace TheIsland.Visual
|
|||||||
{
|
{
|
||||||
RegeneratePlaceholderSprite();
|
RegeneratePlaceholderSprite();
|
||||||
}
|
}
|
||||||
|
// Only regenerate if using placeholder sprite
|
||||||
|
if (characterSprite == null && _spriteRenderer != null)
|
||||||
|
{
|
||||||
|
RegeneratePlaceholderSprite();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phase 21-B: Update Relationship Data
|
||||||
|
if (data.relationships != null)
|
||||||
|
{
|
||||||
|
// Clear and rebuild to ensure freshness
|
||||||
|
_relationships.Clear();
|
||||||
|
foreach (var r in data.relationships)
|
||||||
|
{
|
||||||
|
_relationships[r.target_id] = r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (_moodText != null)
|
if (_moodText != null)
|
||||||
{
|
{
|
||||||
string moodIndicator = GetMoodEmoji(data.mood_state);
|
string moodIndicator = GetMoodEmoji(data.mood_state);
|
||||||
|
|||||||
@@ -90,8 +90,11 @@ namespace TheIsland.Core
|
|||||||
if (agent.IsAlive) count++;
|
if (agent.IsAlive) count++;
|
||||||
}
|
}
|
||||||
return count;
|
return count;
|
||||||
|
return count;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Dictionary<int, AgentVisual> AllAgentVisuals => _agentVisuals;
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Unity Lifecycle
|
#region Unity Lifecycle
|
||||||
|
|||||||
@@ -61,10 +61,24 @@ namespace TheIsland.Models
|
|||||||
|
|
||||||
// Shelter System (Phase 20-B)
|
// Shelter System (Phase 20-B)
|
||||||
public bool is_sheltered;
|
public bool is_sheltered;
|
||||||
|
|
||||||
|
// Phase 21-B: Relationships Sync
|
||||||
|
public List<RelationshipData> relationships;
|
||||||
|
|
||||||
public bool IsAlive => status == "Alive";
|
public bool IsAlive => status == "Alive";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Relationship entry for Phase 21-B.
|
||||||
|
/// </summary>
|
||||||
|
[Serializable]
|
||||||
|
public class RelationshipData
|
||||||
|
{
|
||||||
|
public int target_id;
|
||||||
|
public string type; // "friend", "close_friend", "rival"
|
||||||
|
public int affection;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Agents update event data.
|
/// Agents update event data.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ namespace TheIsland
|
|||||||
private Vector3 _currentVelocity;
|
private Vector3 _currentVelocity;
|
||||||
private float _velocityPercentage; // 0 to 1
|
private float _velocityPercentage; // 0 to 1
|
||||||
private bool _isMoving;
|
private bool _isMoving;
|
||||||
|
private bool _isDancing; // Phase 21
|
||||||
|
private bool _isWaving; // Phase 21
|
||||||
private float _jiggleOffset;
|
private float _jiggleOffset;
|
||||||
private float _jiggleVelocity;
|
private float _jiggleVelocity;
|
||||||
|
|
||||||
@@ -112,6 +114,12 @@ namespace TheIsland
|
|||||||
{
|
{
|
||||||
AnimateIdle();
|
AnimateIdle();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phase 21: Override for Dance/Wave
|
||||||
|
if (_isDancing) AnimateDance();
|
||||||
|
if (_isWaving) AnimateWave();
|
||||||
|
|
||||||
|
// Smoothly apply transforms
|
||||||
|
|
||||||
// Smoothly apply transforms
|
// Smoothly apply transforms
|
||||||
float lerpSpeed = 12f;
|
float lerpSpeed = 12f;
|
||||||
@@ -156,6 +164,47 @@ namespace TheIsland
|
|||||||
_targetScale = new Vector3(_originalScale.x * squash, _originalScale.y * stretch, _originalScale.z);
|
_targetScale = new Vector3(_originalScale.x * squash, _originalScale.y * stretch, _originalScale.z);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phase 21: Social Actions
|
||||||
|
public void SetDancing(bool dancing)
|
||||||
|
{
|
||||||
|
_isDancing = dancing;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetWaving(bool waving)
|
||||||
|
{
|
||||||
|
_isWaving = waving;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AnimateDance()
|
||||||
|
{
|
||||||
|
// Fast rhythmic bounce (130 BPM style)
|
||||||
|
float cycle = Time.time * 15f;
|
||||||
|
float danceBounce = Mathf.Abs(Mathf.Sin(cycle)) * 0.15f;
|
||||||
|
float danceTilt = Mathf.Sin(cycle * 0.5f) * 10f;
|
||||||
|
|
||||||
|
// Apply dance transforms
|
||||||
|
_targetLocalPos = new Vector3(0, danceBounce, 0);
|
||||||
|
_targetLocalRot = Quaternion.Euler(0, 0, danceTilt);
|
||||||
|
|
||||||
|
// Strong squash/stretch on the beat
|
||||||
|
float stretch = 1f + danceBounce * 1.5f;
|
||||||
|
float squash = 1f / stretch;
|
||||||
|
_targetScale = new Vector3(_originalScale.x * squash, _originalScale.y * stretch, _originalScale.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AnimateWave()
|
||||||
|
{
|
||||||
|
// Fast waving rotation
|
||||||
|
float waveCycle = Time.time * 20f;
|
||||||
|
float waveTilt = Mathf.Sin(waveCycle) * 20f; // Exaggerated tilt
|
||||||
|
|
||||||
|
_targetLocalRot = Quaternion.Euler(0, 0, waveTilt);
|
||||||
|
|
||||||
|
// Slight jump for excitement
|
||||||
|
float jumpCurve = Mathf.Abs(Mathf.Sin(Time.time * 5f)) * 0.1f;
|
||||||
|
_targetLocalPos = new Vector3(0, jumpCurve, 0);
|
||||||
|
}
|
||||||
|
|
||||||
private IEnumerator ActionPulseRoutine(float duration, float targetScaleY)
|
private IEnumerator ActionPulseRoutine(float duration, float targetScaleY)
|
||||||
{
|
{
|
||||||
float elapsed = 0;
|
float elapsed = 0;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using Unity.AI.Navigation;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
using UnityEngine.AI;
|
||||||
using TheIsland.Core;
|
using TheIsland.Core;
|
||||||
using TheIsland.Network;
|
using TheIsland.Network;
|
||||||
using TheIsland.Models;
|
using TheIsland.Models;
|
||||||
@@ -76,6 +78,9 @@ namespace TheIsland.Visual
|
|||||||
private Color _targetSkyTop, _targetSkyBottom;
|
private Color _targetSkyTop, _targetSkyBottom;
|
||||||
private Color _currentSkyTop, _currentSkyBottom;
|
private Color _currentSkyTop, _currentSkyBottom;
|
||||||
private List<Transform> _palmTrees = new List<Transform>();
|
private List<Transform> _palmTrees = new List<Transform>();
|
||||||
|
|
||||||
|
// Phase 20-F: NavMesh Surface
|
||||||
|
private NavMeshSurface _navMeshSurface;
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Unity Lifecycle
|
#region Unity Lifecycle
|
||||||
@@ -115,6 +120,12 @@ namespace TheIsland.Visual
|
|||||||
{
|
{
|
||||||
new GameObject("VisualEffectsManager").AddComponent<VisualEffectsManager>();
|
new GameObject("VisualEffectsManager").AddComponent<VisualEffectsManager>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Application.isPlaying)
|
||||||
|
{
|
||||||
|
// Phase 20-F: Build NavMesh at Runtime
|
||||||
|
BuildRuntimeNavMesh();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Update()
|
private void Update()
|
||||||
@@ -203,6 +214,23 @@ namespace TheIsland.Visual
|
|||||||
CreateClouds();
|
CreateClouds();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void BuildRuntimeNavMesh()
|
||||||
|
{
|
||||||
|
// Ensure we have a NavMeshSurface component
|
||||||
|
if (_navMeshSurface == null)
|
||||||
|
{
|
||||||
|
_navMeshSurface = gameObject.AddComponent<NavMeshSurface>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure for 2D/2.5D agent
|
||||||
|
_navMeshSurface.useGeometry = NavMeshCollectGeometry.PhysicsColliders;
|
||||||
|
_navMeshSurface.collectObjects = CollectObjects.Children; // Collect ground and obstacles
|
||||||
|
|
||||||
|
// Rebuild
|
||||||
|
_navMeshSurface.BuildNavMesh();
|
||||||
|
Debug.Log("[EnvironmentManager] Runtime NavMesh Built.");
|
||||||
|
}
|
||||||
|
|
||||||
private void CreateSky()
|
private void CreateSky()
|
||||||
{
|
{
|
||||||
// Create a gradient sky using a camera background shader
|
// Create a gradient sky using a camera background shader
|
||||||
@@ -219,7 +247,7 @@ namespace TheIsland.Visual
|
|||||||
Destroy(skyObj.GetComponent<Collider>());
|
Destroy(skyObj.GetComponent<Collider>());
|
||||||
|
|
||||||
// Create gradient material
|
// Create gradient material
|
||||||
_skyMaterial = CreateGradientMaterial();
|
_skyMaterial = CreateGradientTextureMaterial();
|
||||||
skyObj.GetComponent<Renderer>().material = _skyMaterial;
|
skyObj.GetComponent<Renderer>().material = _skyMaterial;
|
||||||
skyObj.GetComponent<Renderer>().sortingOrder = -100;
|
skyObj.GetComponent<Renderer>().sortingOrder = -100;
|
||||||
|
|
||||||
@@ -449,6 +477,13 @@ namespace TheIsland.Visual
|
|||||||
float spriteHeightUnits = trunkRenderer.sprite.rect.height / trunkRenderer.sprite.pixelsPerUnit;
|
float spriteHeightUnits = trunkRenderer.sprite.rect.height / trunkRenderer.sprite.pixelsPerUnit;
|
||||||
float normScale = scale / spriteHeightUnits;
|
float normScale = scale / spriteHeightUnits;
|
||||||
trunkSprite.transform.localScale = new Vector3(normScale, normScale, 1);
|
trunkSprite.transform.localScale = new Vector3(normScale, normScale, 1);
|
||||||
|
|
||||||
|
// Phase 20-F: NavMesh Obstacle
|
||||||
|
var obstacle = treeObj.AddComponent<NavMeshObstacle>();
|
||||||
|
obstacle.shape = NavMeshObstacleShape.Box;
|
||||||
|
obstacle.center = new Vector3(0, 0.5f * scale, 0); // Center at base, scaled height
|
||||||
|
obstacle.size = new Vector3(0.5f * normScale, 1f * scale, 0.5f * normScale); // Trunk size, scaled
|
||||||
|
obstacle.carving = true; // Force agents to walk around
|
||||||
}
|
}
|
||||||
|
|
||||||
private Texture2D _envTexture;
|
private Texture2D _envTexture;
|
||||||
@@ -630,6 +665,13 @@ namespace TheIsland.Visual
|
|||||||
float spriteWidthUnits = rockRenderer.sprite.rect.width / rockRenderer.sprite.pixelsPerUnit;
|
float spriteWidthUnits = rockRenderer.sprite.rect.width / rockRenderer.sprite.pixelsPerUnit;
|
||||||
float normScale = scale / spriteWidthUnits;
|
float normScale = scale / spriteWidthUnits;
|
||||||
rockObj.transform.localScale = Vector3.one * normScale;
|
rockObj.transform.localScale = Vector3.one * normScale;
|
||||||
|
|
||||||
|
// Phase 20-F: NavMesh Obstacle
|
||||||
|
var obstacle = rockObj.AddComponent<NavMeshObstacle>();
|
||||||
|
obstacle.shape = NavMeshObstacleShape.Box;
|
||||||
|
obstacle.center = new Vector3(0, 0.25f * scale, 0); // Center at base, scaled height
|
||||||
|
obstacle.size = new Vector3(0.8f * normScale, 0.5f * scale, 0.8f * normScale); // Rock size, scaled
|
||||||
|
obstacle.carving = true; // Force agents to walk around
|
||||||
}
|
}
|
||||||
|
|
||||||
private Sprite CreateRockSprite()
|
private Sprite CreateRockSprite()
|
||||||
|
|||||||
@@ -3,9 +3,11 @@
|
|||||||
"com.coplaydev.unity-mcp": "https://github.com/CoplayDev/unity-mcp.git?path=/MCPForUnity",
|
"com.coplaydev.unity-mcp": "https://github.com/CoplayDev/unity-mcp.git?path=/MCPForUnity",
|
||||||
"com.endel.nativewebsocket": "https://github.com/endel/NativeWebSocket.git#upm",
|
"com.endel.nativewebsocket": "https://github.com/endel/NativeWebSocket.git#upm",
|
||||||
"com.unity.2d.enhancers": "1.0.0",
|
"com.unity.2d.enhancers": "1.0.0",
|
||||||
|
"com.unity.ai.navigation": "2.0.9",
|
||||||
"com.unity.feature.2d": "2.0.2",
|
"com.unity.feature.2d": "2.0.2",
|
||||||
"com.unity.inputsystem": "1.17.0",
|
"com.unity.inputsystem": "1.17.0",
|
||||||
"com.unity.multiplayer.center": "1.0.1",
|
"com.unity.multiplayer.center": "1.0.1",
|
||||||
|
"com.unity.postprocessing": "3.4.0",
|
||||||
"com.unity.textmeshpro": "3.0.6",
|
"com.unity.textmeshpro": "3.0.6",
|
||||||
"com.unity.modules.accessibility": "1.0.0",
|
"com.unity.modules.accessibility": "1.0.0",
|
||||||
"com.unity.modules.adaptiveperformance": "1.0.0",
|
"com.unity.modules.adaptiveperformance": "1.0.0",
|
||||||
@@ -40,7 +42,6 @@
|
|||||||
"com.unity.modules.video": "1.0.0",
|
"com.unity.modules.video": "1.0.0",
|
||||||
"com.unity.modules.vr": "1.0.0",
|
"com.unity.modules.vr": "1.0.0",
|
||||||
"com.unity.modules.wind": "1.0.0",
|
"com.unity.modules.wind": "1.0.0",
|
||||||
"com.unity.modules.xr": "1.0.0",
|
"com.unity.modules.xr": "1.0.0"
|
||||||
"com.unity.postprocessing": "3.4.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,6 +147,15 @@
|
|||||||
},
|
},
|
||||||
"url": "https://packages.unity.com"
|
"url": "https://packages.unity.com"
|
||||||
},
|
},
|
||||||
|
"com.unity.ai.navigation": {
|
||||||
|
"version": "2.0.9",
|
||||||
|
"depth": 0,
|
||||||
|
"source": "registry",
|
||||||
|
"dependencies": {
|
||||||
|
"com.unity.modules.ai": "1.0.0"
|
||||||
|
},
|
||||||
|
"url": "https://packages.unity.com"
|
||||||
|
},
|
||||||
"com.unity.ai.toolkit": {
|
"com.unity.ai.toolkit": {
|
||||||
"version": "1.0.0-pre.12",
|
"version": "1.0.0-pre.12",
|
||||||
"depth": 2,
|
"depth": 2,
|
||||||
|
|||||||
Reference in New Issue
Block a user