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:
empty
2026-01-01 23:28:38 +08:00
parent 432f178fc5
commit 8277778106
13 changed files with 927 additions and 784 deletions

View File

@@ -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

View File

@@ -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] 初始化完成");
}

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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;

View File

@@ -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