using System.Collections;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using TheIsland.Models;
using TheIsland.Network;
namespace TheIsland.Visual
{
///
/// Complete 2.5D Agent visual system.
/// Creates sprite, floating UI, and speech bubble programmatically.
/// Attach to an empty GameObject to create a full agent visual.
///
public class AgentVisual : MonoBehaviour
{
#region Configuration
[Header("Sprite Settings")]
[Tooltip("Assign a sprite, or leave empty for auto-generated placeholder")]
[SerializeField] private Sprite characterSprite;
[SerializeField] private Color spriteColor = Color.white;
[SerializeField] private float spriteScale = 2f;
[SerializeField] private int sortingOrder = 10;
[Header("Placeholder Colors (if no sprite assigned)")]
[SerializeField] private Color placeholderBodyColor = new Color(0.3f, 0.6f, 0.9f);
[SerializeField] private Color placeholderOutlineColor = new Color(0.2f, 0.4f, 0.7f);
[Header("UI Settings")]
[SerializeField] private Vector3 uiOffset = new Vector3(0, 2.2f, 0);
[SerializeField] private float uiScale = 0.008f;
[Header("Speech Bubble")]
[SerializeField] private float speechDuration = 5f;
[SerializeField] private Vector3 speechOffset = new Vector3(0, 3.5f, 0);
[Header("Colors")]
[SerializeField] private Color hpHighColor = new Color(0.3f, 0.9f, 0.3f);
[SerializeField] private Color hpLowColor = new Color(0.9f, 0.3f, 0.3f);
[SerializeField] private Color energyHighColor = new Color(1f, 0.8f, 0.2f);
[SerializeField] private Color energyLowColor = new Color(1f, 0.5f, 0.1f);
[Header("Mood Colors")]
[SerializeField] private Color moodHappyColor = new Color(0.3f, 0.9f, 0.5f);
[SerializeField] private Color moodNeutralColor = new Color(0.98f, 0.75f, 0.15f);
[SerializeField] private Color moodSadColor = new Color(0.4f, 0.65f, 0.98f);
[SerializeField] private Color moodAnxiousColor = new Color(0.97f, 0.53f, 0.53f);
#endregion
#region References
private SpriteRenderer _spriteRenderer;
private Canvas _uiCanvas;
private TextMeshProUGUI _nameLabel;
private TextMeshProUGUI _personalityLabel;
private Image _hpBarFill;
private Image _energyBarFill;
private Image _moodBarFill;
private TextMeshProUGUI _hpText;
private TextMeshProUGUI _energyText;
private TextMeshProUGUI _moodText;
private TextMeshProUGUI _moodEmoji;
private GameObject _deathOverlay;
private SpeechBubble _speechBubble;
private Billboard _spriteBillboard;
private Billboard _uiBillboard;
private Camera _mainCamera;
#endregion
#region State
private int _agentId;
private AgentData _currentData;
private Coroutine _speechCoroutine;
// Animation state
private float _idleAnimTimer;
private float _breathScale = 1f;
private Vector3 _originalSpriteScale;
private float _bobOffset;
#endregion
#region Properties
public int AgentId => _agentId;
public AgentData CurrentData => _currentData;
public bool IsAlive => _currentData?.IsAlive ?? false;
#endregion
#region Unity Lifecycle
private void Awake()
{
_mainCamera = Camera.main;
CreateVisuals();
}
private void Update()
{
if (!IsAlive) return;
// Idle breathing animation
_idleAnimTimer += Time.deltaTime;
_breathScale = 1f + Mathf.Sin(_idleAnimTimer * 2f) * 0.02f;
// Gentle bobbing
_bobOffset = Mathf.Sin(_idleAnimTimer * 1.5f) * 0.05f;
if (_spriteRenderer != null && _originalSpriteScale != Vector3.zero)
{
// Apply breathing scale
_spriteRenderer.transform.localScale = new Vector3(
_originalSpriteScale.x * _breathScale,
_originalSpriteScale.y * _breathScale,
_originalSpriteScale.z
);
// Apply bobbing
var pos = _spriteRenderer.transform.localPosition;
pos.y = 1f + _bobOffset;
_spriteRenderer.transform.localPosition = pos;
}
}
private void OnMouseDown()
{
if (!IsAlive)
{
Debug.Log($"[AgentVisual] Cannot interact with dead agent: {_currentData?.name}");
return;
}
NetworkManager.Instance?.FeedAgent(_currentData.name);
Debug.Log($"[AgentVisual] Clicked to feed: {_currentData?.name}");
}
#endregion
#region Initialization
public void Initialize(AgentData data)
{
_agentId = data.id;
_currentData = data;
gameObject.name = $"Agent_{data.id}_{data.name}";
// Apply unique color based on agent ID
ApplyAgentColor(data.id);
// Set UI text
if (_nameLabel != null) _nameLabel.text = data.name;
if (_personalityLabel != null) _personalityLabel.text = $"({data.personality})";
UpdateStats(data);
Debug.Log($"[AgentVisual] Initialized: {data.name}");
}
private void ApplyAgentColor(int agentId)
{
// Generate unique color per agent
Color[] agentColors = new Color[]
{
new Color(0.3f, 0.6f, 0.9f), // Blue (Jack)
new Color(0.9f, 0.5f, 0.7f), // Pink (Luna)
new Color(0.5f, 0.8f, 0.5f), // Green (Bob)
new Color(0.9f, 0.7f, 0.3f), // Orange
new Color(0.7f, 0.5f, 0.9f), // Purple
};
int colorIndex = agentId % agentColors.Length;
placeholderBodyColor = agentColors[colorIndex];
placeholderOutlineColor = agentColors[colorIndex] * 0.7f;
// Update sprite color if using placeholder
if (_spriteRenderer != null && characterSprite == null)
{
RegeneratePlaceholderSprite();
}
}
#endregion
#region Visual Creation
private void CreateVisuals()
{
CreateSprite();
CreateUICanvas();
CreateSpeechBubble();
CreateCollider();
}
private void CreateSprite()
{
// Create sprite child object
var spriteObj = new GameObject("CharacterSprite");
spriteObj.transform.SetParent(transform);
spriteObj.transform.localPosition = new Vector3(0, 1f, 0);
spriteObj.transform.localScale = Vector3.one * spriteScale;
_spriteRenderer = spriteObj.AddComponent();
_spriteRenderer.sortingOrder = sortingOrder;
if (characterSprite != null)
{
_spriteRenderer.sprite = characterSprite;
_spriteRenderer.color = spriteColor;
}
else
{
// Generate placeholder sprite
RegeneratePlaceholderSprite();
}
// Store original scale for animation
_originalSpriteScale = spriteObj.transform.localScale;
// Add billboard
_spriteBillboard = spriteObj.AddComponent();
// Add shadow
CreateShadow(spriteObj.transform);
}
private void CreateShadow(Transform spriteTransform)
{
var shadowObj = new GameObject("Shadow");
shadowObj.transform.SetParent(transform);
shadowObj.transform.localPosition = new Vector3(0, 0.01f, 0);
shadowObj.transform.localRotation = Quaternion.Euler(90, 0, 0);
shadowObj.transform.localScale = new Vector3(1.2f, 0.6f, 1f);
var shadowRenderer = shadowObj.AddComponent();
shadowRenderer.sprite = CreateShadowSprite();
shadowRenderer.sortingOrder = sortingOrder - 1;
shadowRenderer.color = new Color(0, 0, 0, 0.3f);
}
private Sprite CreateShadowSprite()
{
int size = 32;
Texture2D tex = new Texture2D(size, size);
tex.filterMode = FilterMode.Bilinear;
Vector2 center = new Vector2(size / 2f, size / 2f);
Color[] pixels = new Color[size * size];
for (int y = 0; y < size; y++)
{
for (int x = 0; x < size; x++)
{
float dx = (x - center.x) / (size * 0.4f);
float dy = (y - center.y) / (size * 0.4f);
float dist = dx * dx + dy * dy;
if (dist < 1)
{
float alpha = Mathf.Clamp01(1 - dist) * 0.5f;
pixels[y * size + x] = new Color(0, 0, 0, alpha);
}
else
{
pixels[y * size + x] = Color.clear;
}
}
}
tex.SetPixels(pixels);
tex.Apply();
return Sprite.Create(tex, new Rect(0, 0, size, size), new Vector2(0.5f, 0.5f), 100f);
}
private void RegeneratePlaceholderSprite()
{
if (_spriteRenderer == null) return;
// Create a simple character placeholder (circle with body shape)
Texture2D texture = CreatePlaceholderTexture(64, 64);
_spriteRenderer.sprite = Sprite.Create(
texture,
new Rect(0, 0, texture.width, texture.height),
new Vector2(0.5f, 0.5f),
100f
);
}
private Texture2D CreatePlaceholderTexture(int width, int height)
{
Texture2D texture = new Texture2D(width, height, TextureFormat.RGBA32, false);
texture.filterMode = FilterMode.Bilinear;
// Clear to transparent
Color[] pixels = new Color[width * height];
for (int i = 0; i < pixels.Length; i++)
{
pixels[i] = Color.clear;
}
Vector2 center = new Vector2(width / 2f, height / 2f);
// Create highlight and shadow colors
Color highlight = Color.Lerp(placeholderBodyColor, Color.white, 0.3f);
Color shadow = Color.Lerp(placeholderBodyColor, Color.black, 0.3f);
Color skinTone = new Color(0.95f, 0.8f, 0.7f);
Color skinShadow = new Color(0.85f, 0.65f, 0.55f);
// Body (ellipse with shading)
Vector2 bodyCenter = center + Vector2.down * 6;
DrawShadedEllipse(pixels, width, height, bodyCenter, 16, 22, placeholderBodyColor, highlight, shadow);
// Head (circle with skin tone)
Vector2 headCenter = center + Vector2.up * 14;
DrawShadedCircle(pixels, width, height, headCenter, 13, skinTone, Color.Lerp(skinTone, Color.white, 0.2f), skinShadow);
// Hair (top of head)
Color hairColor = placeholderOutlineColor;
DrawHair(pixels, width, height, headCenter, 13, hairColor);
// Eyes
DrawCircle(pixels, width, height, headCenter + new Vector2(-4, -1), 3, Color.white);
DrawCircle(pixels, width, height, headCenter + new Vector2(4, -1), 3, Color.white);
DrawCircle(pixels, width, height, headCenter + new Vector2(-4, -1), 1.5f, new Color(0.2f, 0.15f, 0.1f));
DrawCircle(pixels, width, height, headCenter + new Vector2(4, -1), 1.5f, new Color(0.2f, 0.15f, 0.1f));
// Eye highlights
DrawCircle(pixels, width, height, headCenter + new Vector2(-3, 0), 0.8f, Color.white);
DrawCircle(pixels, width, height, headCenter + new Vector2(5, 0), 0.8f, Color.white);
// Mouth (smile)
DrawSmile(pixels, width, height, headCenter + Vector2.down * 5, 4);
// Blush
DrawCircle(pixels, width, height, headCenter + new Vector2(-7, -3), 2, new Color(1f, 0.6f, 0.6f, 0.4f));
DrawCircle(pixels, width, height, headCenter + new Vector2(7, -3), 2, new Color(1f, 0.6f, 0.6f, 0.4f));
// Arms
DrawArm(pixels, width, height, bodyCenter + new Vector2(-14, 5), -30, skinTone);
DrawArm(pixels, width, height, bodyCenter + new Vector2(14, 5), 30, skinTone);
// Legs
DrawLeg(pixels, width, height, bodyCenter + new Vector2(-6, -20), placeholderBodyColor);
DrawLeg(pixels, width, height, bodyCenter + new Vector2(6, -20), placeholderBodyColor);
// Outline
AddOutline(pixels, width, height, placeholderOutlineColor);
texture.SetPixels(pixels);
texture.Apply();
return texture;
}
private void DrawShadedCircle(Color[] pixels, int width, int height, Vector2 center, float radius, Color baseColor, Color highlight, Color shadow)
{
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
float dist = Vector2.Distance(new Vector2(x, y), center);
if (dist <= radius)
{
// Shading based on position relative to light source (top-left)
float dx = (x - center.x) / radius;
float dy = (y - center.y) / radius;
float shade = (-dx * 0.3f + dy * 0.7f) * 0.5f + 0.5f;
Color color = Color.Lerp(highlight, shadow, shade);
color = Color.Lerp(color, baseColor, 0.5f);
pixels[y * width + x] = color;
}
}
}
}
private void DrawShadedEllipse(Color[] pixels, int width, int height, Vector2 center, float rx, float ry, Color baseColor, Color highlight, Color shadow)
{
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
float dx = (x - center.x) / rx;
float dy = (y - center.y) / ry;
if (dx * dx + dy * dy <= 1)
{
float shade = (-dx * 0.3f + dy * 0.5f) * 0.5f + 0.5f;
Color color = Color.Lerp(highlight, shadow, shade);
color = Color.Lerp(color, baseColor, 0.5f);
pixels[y * width + x] = color;
}
}
}
}
private void DrawHair(Color[] pixels, int width, int height, Vector2 headCenter, float headRadius, Color hairColor)
{
// Draw hair on top half of head
for (int y = (int)(headCenter.y); y < height; y++)
{
for (int x = 0; x < width; x++)
{
float dist = Vector2.Distance(new Vector2(x, y), headCenter);
if (dist <= headRadius + 2 && dist >= headRadius - 4 && y > headCenter.y - 2)
{
float noise = Mathf.PerlinNoise(x * 0.3f, y * 0.3f);
if (noise > 0.3f)
{
pixels[y * width + x] = Color.Lerp(hairColor, hairColor * 0.7f, noise);
}
}
}
}
}
private void DrawSmile(Color[] pixels, int width, int height, Vector2 center, float smileWidth)
{
Color mouthColor = new Color(0.8f, 0.4f, 0.4f);
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);
if (x >= 0 && x < width && y >= 0 && y < height)
{
pixels[y * width + x] = mouthColor;
if (y > 0) pixels[(y - 1) * width + x] = mouthColor;
}
}
}
private void DrawArm(Color[] pixels, int width, int height, Vector2 start, float angle, Color skinColor)
{
float rad = angle * Mathf.Deg2Rad;
int length = 10;
for (int i = 0; i < length; i++)
{
int x = (int)(start.x + Mathf.Sin(rad) * i);
int y = (int)(start.y - Mathf.Cos(rad) * i);
DrawCircle(pixels, width, height, new Vector2(x, y), 2, skinColor);
}
}
private void DrawLeg(Color[] pixels, int width, int height, Vector2 start, Color clothColor)
{
for (int i = 0; i < 8; i++)
{
int x = (int)start.x;
int y = (int)(start.y - i);
if (y >= 0 && y < height)
{
DrawCircle(pixels, width, height, new Vector2(x, y), 3, clothColor);
}
}
// Shoe
DrawCircle(pixels, width, height, start + Vector2.down * 8, 4, new Color(0.3f, 0.2f, 0.15f));
}
private void AddOutline(Color[] pixels, int width, int height, Color outlineColor)
{
Color[] newPixels = (Color[])pixels.Clone();
for (int y = 1; y < height - 1; y++)
{
for (int x = 1; x < width - 1; x++)
{
if (pixels[y * width + x].a < 0.1f)
{
// Check neighbors
bool hasNeighbor = false;
for (int dy = -1; dy <= 1; dy++)
{
for (int dx = -1; dx <= 1; dx++)
{
if (pixels[(y + dy) * width + (x + dx)].a > 0.5f)
{
hasNeighbor = true;
break;
}
}
if (hasNeighbor) break;
}
if (hasNeighbor)
{
newPixels[y * width + x] = outlineColor;
}
}
}
}
System.Array.Copy(newPixels, pixels, pixels.Length);
}
private void DrawCircle(Color[] pixels, int width, int height, Vector2 center, float radius, Color color)
{
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
float dist = Vector2.Distance(new Vector2(x, y), center);
if (dist <= radius)
{
pixels[y * width + x] = color;
}
}
}
}
private void DrawCircleOutline(Color[] pixels, int width, int height, Vector2 center, float radius, Color color, int thickness)
{
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
float dist = Vector2.Distance(new Vector2(x, y), center);
if (dist >= radius - thickness && dist <= radius + thickness)
{
pixels[y * width + x] = color;
}
}
}
}
private void DrawEllipse(Color[] pixels, int width, int height, Vector2 center, float rx, float ry, Color color)
{
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
float dx = (x - center.x) / rx;
float dy = (y - center.y) / ry;
if (dx * dx + dy * dy <= 1)
{
pixels[y * width + x] = color;
}
}
}
}
private void DrawEllipseOutline(Color[] pixels, int width, int height, Vector2 center, float rx, float ry, Color color, int thickness)
{
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
float dx = (x - center.x) / rx;
float dy = (y - center.y) / ry;
float dist = dx * dx + dy * dy;
float outer = 1 + (thickness / Mathf.Min(rx, ry));
float inner = 1 - (thickness / Mathf.Min(rx, ry));
if (dist >= inner && dist <= outer)
{
pixels[y * width + x] = color;
}
}
}
}
private void CreateUICanvas()
{
// World Space Canvas
var canvasObj = new GameObject("UICanvas");
canvasObj.transform.SetParent(transform);
canvasObj.transform.localPosition = uiOffset;
canvasObj.transform.localScale = Vector3.one * uiScale;
_uiCanvas = canvasObj.AddComponent