feat: implement survival, crafting, memory, and social systems
- Phase 13: Autonomous Agency - agents now have actions and locations - Phase 15: Sickness mechanics with immunity and weather effects - Phase 16: Crafting system (medicine from herbs) - Phase 17-A: Resource scarcity with tree fruit regeneration - Phase 17-B: Social roles (leader, follower, loner) with clique behavior - Phase 17-C: Random events support - Add AgentMemory model for long-term agent memory storage - Add memory_service for managing agent memories - Update Unity client models and event handlers 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -69,6 +69,7 @@ namespace TheIsland.Visual
|
||||
#region State
|
||||
private int _agentId;
|
||||
private AgentData _currentData;
|
||||
private string _moodState = "neutral";
|
||||
private Coroutine _speechCoroutine;
|
||||
|
||||
// Animation state
|
||||
@@ -76,6 +77,11 @@ namespace TheIsland.Visual
|
||||
private float _breathScale = 1f;
|
||||
private Vector3 _originalSpriteScale;
|
||||
private float _bobOffset;
|
||||
|
||||
// Movement state
|
||||
private Vector3 _targetPosition;
|
||||
private bool _isMoving;
|
||||
private float _moveSpeed = 3f;
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
@@ -95,6 +101,29 @@ namespace TheIsland.Visual
|
||||
{
|
||||
if (!IsAlive) return;
|
||||
|
||||
// Handle Movement
|
||||
if (_isMoving)
|
||||
{
|
||||
transform.position = Vector3.MoveTowards(transform.position, _targetPosition, _moveSpeed * Time.deltaTime);
|
||||
|
||||
// Flip sprite based on direction
|
||||
if (_spriteRenderer != null)
|
||||
{
|
||||
float dx = _targetPosition.x - transform.position.x;
|
||||
if (Mathf.Abs(dx) > 0.1f)
|
||||
{
|
||||
// FlipX = true means face Left (assuming sprite faces Right by default)
|
||||
// If sprite faces Front, we might need a different approach, but FlipX is standard for 2D.
|
||||
_spriteRenderer.flipX = dx < 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (Vector3.Distance(transform.position, _targetPosition) < 0.05f)
|
||||
{
|
||||
_isMoving = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Idle breathing animation (Squash and Stretch)
|
||||
_idleAnimTimer += Time.deltaTime;
|
||||
|
||||
@@ -103,8 +132,16 @@ namespace TheIsland.Visual
|
||||
_breathScale = 1f + breath;
|
||||
float antiBreath = 1f - (breath * 0.5f); // Squash X when stretching Y
|
||||
|
||||
// Bobbing: Move up and down
|
||||
_bobOffset = Mathf.Sin(_idleAnimTimer * 2f) * 0.08f;
|
||||
// Bobbing: Move up and down (only when idle)
|
||||
if (!_isMoving)
|
||||
{
|
||||
_bobOffset = Mathf.Sin(_idleAnimTimer * 2f) * 0.08f;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Hop while moving
|
||||
_bobOffset = Mathf.Abs(Mathf.Sin(_idleAnimTimer * 10f)) * 0.2f;
|
||||
}
|
||||
|
||||
if (_spriteRenderer != null && _originalSpriteScale != Vector3.zero)
|
||||
{
|
||||
@@ -128,6 +165,18 @@ namespace TheIsland.Visual
|
||||
StartCoroutine(JumpRoutine());
|
||||
}
|
||||
|
||||
public void MoveTo(Vector3 target)
|
||||
{
|
||||
_targetPosition = target;
|
||||
// Keep current Y (height) to avoid sinking/flying, unless target specifies it
|
||||
// Actually our agents are on navmesh or free moving? Free moving for now.
|
||||
// But we want to keep them on the "ground" plane roughly.
|
||||
// Let's preserve current Y if target Y is 0 (which usually means undefined in 2D topdown logic, but here we are 2.5D)
|
||||
// The spawn positions have Y=0.
|
||||
_targetPosition.y = transform.position.y;
|
||||
_isMoving = true;
|
||||
}
|
||||
|
||||
private IEnumerator JumpRoutine()
|
||||
{
|
||||
float timer = 0;
|
||||
@@ -438,7 +487,27 @@ namespace TheIsland.Visual
|
||||
for (int x = (int)(center.x - smileWidth); x <= (int)(center.x + smileWidth); x++)
|
||||
{
|
||||
float t = (x - center.x + smileWidth) / (smileWidth * 2);
|
||||
int y = (int)(center.y - Mathf.Sin(t * Mathf.PI) * 2);
|
||||
int y = (int)center.y;
|
||||
|
||||
// Mouth shape based on mood
|
||||
if (_moodState == "happy")
|
||||
{
|
||||
y = (int)(center.y - Mathf.Sin(t * Mathf.PI) * 2);
|
||||
}
|
||||
else if (_moodState == "sad")
|
||||
{
|
||||
y = (int)(center.y - 2 + Mathf.Sin(t * Mathf.PI) * 2);
|
||||
}
|
||||
else if (_moodState == "anxious")
|
||||
{
|
||||
// Wavy mouth
|
||||
y = (int)(center.y + Mathf.Sin(t * Mathf.PI * 3) * 1);
|
||||
}
|
||||
else // neutral
|
||||
{
|
||||
y = (int)(center.y);
|
||||
}
|
||||
|
||||
if (x >= 0 && x < width && y >= 0 && y < height)
|
||||
{
|
||||
pixels[y * width + x] = mouthColor;
|
||||
@@ -880,6 +949,17 @@ namespace TheIsland.Visual
|
||||
_moodBarFill.rectTransform.anchorMax = new Vector2(moodPercent, 1);
|
||||
_moodBarFill.color = GetMoodColor(data.mood_state);
|
||||
}
|
||||
|
||||
// Check for mood change (Visual Expression)
|
||||
if (_moodState != data.mood_state)
|
||||
{
|
||||
_moodState = data.mood_state;
|
||||
// Only regenerate if using placeholder sprite
|
||||
if (characterSprite == null && _spriteRenderer != null)
|
||||
{
|
||||
RegeneratePlaceholderSprite();
|
||||
}
|
||||
}
|
||||
if (_moodText != null)
|
||||
{
|
||||
string moodIndicator = GetMoodEmoji(data.mood_state);
|
||||
@@ -943,11 +1023,42 @@ namespace TheIsland.Visual
|
||||
{
|
||||
if (_deathOverlay != null) _deathOverlay.SetActive(false);
|
||||
|
||||
// Restore sprite color
|
||||
// Restore sprite color based on state
|
||||
if (_spriteRenderer != null)
|
||||
{
|
||||
_spriteRenderer.color = Color.white;
|
||||
Color targetColor = spriteColor;
|
||||
|
||||
// Phase 15: Sickness visual (Green tint)
|
||||
if (_currentData != null && _currentData.is_sick)
|
||||
{
|
||||
targetColor = Color.Lerp(targetColor, Color.green, 0.4f);
|
||||
}
|
||||
|
||||
_spriteRenderer.color = targetColor;
|
||||
}
|
||||
|
||||
// Phase 17-B: Update social role display
|
||||
UpdateSocialRoleDisplay();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Display social role indicator based on agent's role.
|
||||
/// </summary>
|
||||
private void UpdateSocialRoleDisplay()
|
||||
{
|
||||
if (_currentData == null || _nameLabel == null) return;
|
||||
|
||||
string roleIcon = _currentData.social_role switch
|
||||
{
|
||||
"leader" => " <color=#FFD700>★</color>", // Gold star
|
||||
"loner" => " <color=#808080>☁</color>", // Gray cloud
|
||||
"follower" => " <color=#87CEEB>→</color>", // Sky blue arrow
|
||||
_ => ""
|
||||
};
|
||||
|
||||
// Append role icon to name (strip any existing icons first)
|
||||
string baseName = _currentData.name;
|
||||
_nameLabel.text = baseName + roleIcon;
|
||||
}
|
||||
#endregion
|
||||
|
||||
|
||||
@@ -38,7 +38,6 @@ namespace TheIsland.UI
|
||||
private List<GameObject> _entries = new List<GameObject>();
|
||||
private bool _visible = true;
|
||||
private int _unread = 0;
|
||||
private bool _ready = false;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
@@ -83,7 +82,6 @@ namespace TheIsland.UI
|
||||
if (NetworkManager.Instance != null)
|
||||
{
|
||||
SubscribeEvents();
|
||||
_ready = true;
|
||||
AddLog("事件日志已就绪", Color.yellow);
|
||||
Debug.Log("[EventLog] 初始化完成");
|
||||
}
|
||||
|
||||
@@ -153,6 +153,8 @@ namespace TheIsland.Core
|
||||
network.OnRevive += HandleRevive;
|
||||
network.OnSocialInteraction += HandleSocialInteraction;
|
||||
network.OnGiftEffect += HandleGiftEffect; // Phase 8
|
||||
network.OnAgentAction += HandleAgentAction; // Phase 13
|
||||
network.OnRandomEvent += HandleRandomEvent; // Phase 17-C
|
||||
}
|
||||
|
||||
private void UnsubscribeFromNetworkEvents()
|
||||
@@ -180,6 +182,7 @@ namespace TheIsland.Core
|
||||
network.OnRevive -= HandleRevive;
|
||||
network.OnSocialInteraction -= HandleSocialInteraction;
|
||||
network.OnGiftEffect -= HandleGiftEffect; // Phase 8
|
||||
network.OnRandomEvent -= HandleRandomEvent; // Phase 17-C
|
||||
}
|
||||
#endregion
|
||||
|
||||
@@ -536,6 +539,92 @@ namespace TheIsland.Core
|
||||
// Show notification
|
||||
ShowNotification(data.message);
|
||||
}
|
||||
#region Agent Action (Phase 13)
|
||||
private void HandleAgentAction(AgentActionData data)
|
||||
{
|
||||
Debug.Log($"[GameManager] Action: {data.agent_name} -> {data.action_type} at {data.location}");
|
||||
|
||||
// Resolve target position
|
||||
Vector3 targetPos = GetLocationPosition(data.location, data.target_name);
|
||||
|
||||
// Find agent and command movement
|
||||
if (_agentVisuals.TryGetValue(data.agent_id, out AgentVisual agentVisual))
|
||||
{
|
||||
agentVisual.MoveTo(targetPos);
|
||||
|
||||
// Optional: Show thought bubble or speech
|
||||
if (!string.IsNullOrEmpty(data.dialogue))
|
||||
{
|
||||
agentVisual.ShowSpeech(data.dialogue, 3f);
|
||||
}
|
||||
}
|
||||
else if (_agentUIs.TryGetValue(data.agent_id, out AgentUI agentUI))
|
||||
{
|
||||
// Fallback for UI-only agents (just show speech)
|
||||
if (!string.IsNullOrEmpty(data.dialogue))
|
||||
{
|
||||
agentUI.ShowSpeech(data.dialogue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Vector3 GetLocationPosition(string location, string targetName)
|
||||
{
|
||||
// Map logical locations to world coordinates
|
||||
switch (location.ToLower())
|
||||
{
|
||||
case "tree_left":
|
||||
return new Vector3(-10f, 0f, 8f);
|
||||
case "tree_right":
|
||||
return new Vector3(10f, 0f, 8f);
|
||||
case "campfire":
|
||||
case "center":
|
||||
return new Vector3(0f, 0f, 0f);
|
||||
case "water":
|
||||
case "beach":
|
||||
return new Vector3(Random.Range(-5, 5), 0f, 4f);
|
||||
case "nearby":
|
||||
// Move to random nearby spot (wandering)
|
||||
return new Vector3(Random.Range(-12, 12), 0f, Random.Range(-2, 6));
|
||||
case "herb_patch":
|
||||
// Phase 16: Herb gathering location
|
||||
return new Vector3(-8f, 0f, -5f);
|
||||
case "agent":
|
||||
// Move to another agent
|
||||
int targetId = GetAgentIdByName(targetName);
|
||||
if (targetId >= 0 && _agentVisuals.TryGetValue(targetId, out AgentVisual target))
|
||||
{
|
||||
// Stand slightly offset from target
|
||||
return target.transform.position + new Vector3(1.5f, 0, 0);
|
||||
}
|
||||
return Vector3.zero;
|
||||
default:
|
||||
return new Vector3(0f, 0f, 0f); // Fallback to center
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Random Events (Phase 17-C)
|
||||
private void HandleRandomEvent(RandomEventData data)
|
||||
{
|
||||
Debug.Log($"[GameManager] Random Event: {data.event_type} - {data.message}");
|
||||
|
||||
// Display global notification banner
|
||||
string eventIcon = data.event_type switch
|
||||
{
|
||||
"storm_damage" => "⛈️ ",
|
||||
"treasure_found" => "💎 ",
|
||||
"beast_attack" => "🐺 ",
|
||||
"rumor_spread" => "💬 ",
|
||||
_ => ""
|
||||
};
|
||||
|
||||
ShowNotification(eventIcon + data.message);
|
||||
|
||||
// Optional: Trigger visual effects based on event type
|
||||
// (Could add screen shake for storm, highlight agent for treasure, etc.)
|
||||
}
|
||||
#endregion
|
||||
#endregion
|
||||
|
||||
#region Agent Management
|
||||
|
||||
@@ -48,6 +48,17 @@ namespace TheIsland.Models
|
||||
public string mood_state; // "happy", "neutral", "sad", "anxious"
|
||||
public string social_tendency; // "introvert", "extrovert", "neutral"
|
||||
|
||||
// Survival (Phase 15)
|
||||
public bool is_sick;
|
||||
public int immunity;
|
||||
|
||||
// Autonomous Agency (Phase 13)
|
||||
public string current_action;
|
||||
public string location;
|
||||
|
||||
// Relationship 2.0 (Phase 17-B)
|
||||
public string social_role; // "leader", "follower", "loner", "neutral"
|
||||
|
||||
public bool IsAlive => status == "Alive";
|
||||
}
|
||||
|
||||
@@ -138,6 +149,10 @@ namespace TheIsland.Models
|
||||
public int resource_level;
|
||||
public int current_tick_in_day;
|
||||
public string time_of_day; // "dawn", "day", "dusk", "night"
|
||||
|
||||
// Resource Scarcity (Phase 17-A)
|
||||
public int tree_left_fruit;
|
||||
public int tree_right_fruit;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -311,5 +326,64 @@ namespace TheIsland.Models
|
||||
|
||||
// Gift/Donation system (Phase 8)
|
||||
public const string GIFT_EFFECT = "gift_effect";
|
||||
|
||||
// Autonomous Agency (Phase 13)
|
||||
public const string AGENT_ACTION = "agent_action";
|
||||
|
||||
// Crafting System (Phase 16)
|
||||
public const string CRAFT = "craft";
|
||||
public const string USE_ITEM = "use_item";
|
||||
|
||||
// Random Events (Phase 17-C)
|
||||
public const string RANDOM_EVENT = "random_event";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Agent action event data (Phase 13).
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class AgentActionData
|
||||
{
|
||||
public int agent_id;
|
||||
public string agent_name;
|
||||
public string action_type; // "Gather", "Sleep", "Socialize", "Wander", "Gather Herb", etc.
|
||||
public string location; // "tree_left", "campfire", "herb_patch", etc.
|
||||
public string target_name; // For social actions
|
||||
public string dialogue; // Bark text
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Craft event data (Phase 16).
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class CraftEventData
|
||||
{
|
||||
public int agent_id;
|
||||
public string agent_name;
|
||||
public string item; // "medicine"
|
||||
public string ingredients; // JSON string of ingredients used
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Use item event data (Phase 16).
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class UseItemEventData
|
||||
{
|
||||
public int agent_id;
|
||||
public string agent_name;
|
||||
public string item; // "medicine"
|
||||
public string effect; // "cured sickness"
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Random event data (Phase 17-C).
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class RandomEventData
|
||||
{
|
||||
public string event_type; // "storm_damage", "treasure_found", "beast_attack", "rumor_spread"
|
||||
public string message;
|
||||
public string agent_name; // Optional: affected agent
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +68,10 @@ namespace TheIsland.Network
|
||||
public event Action<SocialInteractionData> OnSocialInteraction;
|
||||
public event Action<WorldStateData> OnWorldUpdate;
|
||||
public event Action<GiftEffectData> OnGiftEffect; // Phase 8: Gift/Donation effects
|
||||
public event Action<AgentActionData> OnAgentAction; // Phase 13: Autonomous Actions
|
||||
public event Action<CraftEventData> OnCraft; // Phase 16: Crafting
|
||||
public event Action<UseItemEventData> OnUseItem; // Phase 16: Using items
|
||||
public event Action<RandomEventData> OnRandomEvent; // Phase 17-C: Random Events
|
||||
#endregion
|
||||
|
||||
#region Private Fields
|
||||
@@ -349,11 +353,32 @@ namespace TheIsland.Network
|
||||
OnGiftEffect?.Invoke(giftData);
|
||||
break;
|
||||
|
||||
case EventTypes.AGENT_ACTION:
|
||||
var actionData = JsonUtility.FromJson<AgentActionData>(dataJson);
|
||||
OnAgentAction?.Invoke(actionData);
|
||||
break;
|
||||
|
||||
case EventTypes.CRAFT:
|
||||
var craftData = JsonUtility.FromJson<CraftEventData>(dataJson);
|
||||
OnCraft?.Invoke(craftData);
|
||||
break;
|
||||
|
||||
case EventTypes.USE_ITEM:
|
||||
var useItemData = JsonUtility.FromJson<UseItemEventData>(dataJson);
|
||||
OnUseItem?.Invoke(useItemData);
|
||||
break;
|
||||
|
||||
case EventTypes.COMMENT:
|
||||
// Comments can be logged but typically not displayed in 3D
|
||||
Debug.Log($"[Chat] {json}");
|
||||
break;
|
||||
|
||||
case EventTypes.RANDOM_EVENT:
|
||||
var randomEventData = JsonUtility.FromJson<RandomEventData>(dataJson);
|
||||
OnRandomEvent?.Invoke(randomEventData);
|
||||
Debug.Log($"[Random Event] {randomEventData.event_type}: {randomEventData.message}");
|
||||
break;
|
||||
|
||||
default:
|
||||
Debug.Log($"[NetworkManager] Unhandled event type: {baseMessage.event_type}");
|
||||
break;
|
||||
|
||||
@@ -56,7 +56,6 @@ namespace TheIsland.Visual
|
||||
[SerializeField] private Color waterShallowColor = new Color(0.3f, 0.8f, 0.9f, 0.8f);
|
||||
[SerializeField] private Color waterDeepColor = new Color(0.1f, 0.4f, 0.6f, 0.9f);
|
||||
[SerializeField] private float waveSpeed = 0.5f;
|
||||
[SerializeField] private float waveAmplitude = 0.1f;
|
||||
[SerializeField] private Material customWaterMaterial; // Custom shader support
|
||||
#endregion
|
||||
|
||||
|
||||
Reference in New Issue
Block a user