Files
the-island/unity-client/Assets/Scripts/GameManager.cs
empty 9d248f2cdb fix: remove extra closing brace in GameManager.cs
Remove duplicate } at line 504 that was breaking the class structure
and causing CS1022 compilation error.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 02:38:06 +08:00

914 lines
31 KiB
C#

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using TheIsland.Models;
using TheIsland.Network;
using TheIsland.Agents;
using TheIsland.UI;
using TheIsland.Visual;
namespace TheIsland.Core
{
/// <summary>
/// Main game controller.
/// Manages agent spawning, UI updates, and event handling.
/// </summary>
public class GameManager : MonoBehaviour
{
#region Singleton
private static GameManager _instance;
public static GameManager Instance => _instance;
#endregion
#region Configuration
[Header("Agent Spawning")]
[SerializeField] private GameObject agentPrefab;
[SerializeField] private Transform agentContainer;
[SerializeField] private Vector3[] spawnPositions = new Vector3[]
{
new Vector3(-3f, 0f, 0f),
new Vector3(0f, 0f, 0f),
new Vector3(3f, 0f, 0f)
};
[Header("UI References")]
[SerializeField] private TextMeshProUGUI connectionStatus;
[SerializeField] private TextMeshProUGUI tickInfo;
[SerializeField] private TextMeshProUGUI goldDisplay;
[SerializeField] private Button resetButton;
[SerializeField] private TMP_InputField commandInput;
[SerializeField] private Button sendButton;
[Header("Notification Panel")]
[SerializeField] private GameObject notificationPanel;
[SerializeField] private TextMeshProUGUI notificationText;
[SerializeField] private float notificationDuration = 3f;
#endregion
#region Private Fields
private Dictionary<int, AgentController> _agents = new Dictionary<int, AgentController>();
private Dictionary<int, AgentUI> _agentUIs = new Dictionary<int, AgentUI>();
private Dictionary<int, AgentVisual> _agentVisuals = new Dictionary<int, AgentVisual>();
private int _playerGold = 100;
private int _currentTick;
private int _currentDay;
private int _nextSpawnIndex;
// World state
private string _currentTimeOfDay = "day";
private string _currentWeather = "Sunny";
#endregion
#region Properties
public int PlayerGold => _playerGold;
public string CurrentTimeOfDay => _currentTimeOfDay;
public string CurrentWeather => _currentWeather;
public int CurrentDay => _currentDay;
public int AliveAgentCount
{
get
{
int count = 0;
// Check AgentVisual first (newest system)
foreach (var visual in _agentVisuals.Values)
{
if (visual.IsAlive) count++;
}
if (count > 0) return count;
// Fallback to AgentUI
foreach (var agentUI in _agentUIs.Values)
{
if (agentUI.IsAlive) count++;
}
if (count > 0) return count;
// Fallback to AgentController (legacy)
foreach (var agent in _agents.Values)
{
if (agent.IsAlive) count++;
}
return count;
return count;
}
}
public Dictionary<int, AgentVisual> AllAgentVisuals => _agentVisuals;
#endregion
#region Unity Lifecycle
private void Awake()
{
if (_instance != null && _instance != this)
{
Destroy(gameObject);
return;
}
_instance = this;
}
private void Start()
{
// Subscribe to network events
SubscribeToNetworkEvents();
// Setup UI
SetupUI();
// Initial connection status
UpdateConnectionStatus(false);
}
private void OnDestroy()
{
// Unsubscribe from network events
UnsubscribeFromNetworkEvents();
// Cleanup UI listeners
CleanupUI();
}
#endregion
#region Network Event Subscription
private void SubscribeToNetworkEvents()
{
var network = NetworkManager.Instance;
if (network == null) return;
network.OnConnected += HandleConnected;
network.OnDisconnected += HandleDisconnected;
network.OnAgentsUpdate += HandleAgentsUpdate;
network.OnAgentSpeak += HandleAgentSpeak;
network.OnAgentDied += HandleAgentDied;
network.OnFeed += HandleFeed;
network.OnTick += HandleTick;
network.OnSystemMessage += HandleSystemMessage;
network.OnUserUpdate += HandleUserUpdate;
// New phase events
network.OnWeatherChange += HandleWeatherChange;
network.OnPhaseChange += HandlePhaseChange;
network.OnDayChange += HandleDayChange;
network.OnHeal += HandleHeal;
network.OnEncourage += HandleEncourage;
network.OnTalk += HandleTalk;
network.OnRevive += HandleRevive;
network.OnSocialInteraction += HandleSocialInteraction;
network.OnGiftEffect += HandleGiftEffect; // Phase 8
network.OnAgentAction += HandleAgentAction; // Phase 13
network.OnRandomEvent += HandleRandomEvent; // Phase 17-C
network.OnGiveItem += HandleGiveItem; // Phase 23
network.OnGroupActivity += HandleGroupActivity; // Phase 24
network.OnVFXEvent += HandleVFXEvent; // Phase 8
}
private void UnsubscribeFromNetworkEvents()
{
var network = NetworkManager.Instance;
if (network == null) return;
network.OnConnected -= HandleConnected;
network.OnDisconnected -= HandleDisconnected;
network.OnAgentsUpdate -= HandleAgentsUpdate;
network.OnAgentSpeak -= HandleAgentSpeak;
network.OnAgentDied -= HandleAgentDied;
network.OnFeed -= HandleFeed;
network.OnTick -= HandleTick;
network.OnSystemMessage -= HandleSystemMessage;
network.OnUserUpdate -= HandleUserUpdate;
// New phase events
network.OnWeatherChange -= HandleWeatherChange;
network.OnPhaseChange -= HandlePhaseChange;
network.OnDayChange -= HandleDayChange;
network.OnHeal -= HandleHeal;
network.OnEncourage -= HandleEncourage;
network.OnTalk -= HandleTalk;
network.OnRevive -= HandleRevive;
network.OnSocialInteraction -= HandleSocialInteraction;
network.OnGiftEffect -= HandleGiftEffect; // Phase 8
network.OnRandomEvent -= HandleRandomEvent; // Phase 17-C
network.OnGiveItem -= HandleGiveItem; // Phase 23
network.OnGroupActivity -= HandleGroupActivity; // Phase 24
network.OnVFXEvent -= HandleVFXEvent; // Phase 8
}
#endregion
#region UI Setup
private void SetupUI()
{
// Reset button
if (resetButton != null)
{
resetButton.onClick.RemoveAllListeners();
resetButton.onClick.AddListener(OnResetClicked);
}
// Send button
if (sendButton != null)
{
sendButton.onClick.RemoveAllListeners();
sendButton.onClick.AddListener(OnSendClicked);
}
// Command input enter key
if (commandInput != null)
{
commandInput.onSubmit.RemoveAllListeners();
commandInput.onSubmit.AddListener(OnCommandSubmit);
}
// Hide notification initially
if (notificationPanel != null)
{
notificationPanel.SetActive(false);
}
UpdateGoldDisplay();
}
private void CleanupUI()
{
if (resetButton != null)
{
resetButton.onClick.RemoveListener(OnResetClicked);
}
if (sendButton != null)
{
sendButton.onClick.RemoveListener(OnSendClicked);
}
if (commandInput != null)
{
commandInput.onSubmit.RemoveListener(OnCommandSubmit);
}
}
private void UpdateConnectionStatus(bool connected)
{
if (connectionStatus == null) return;
connectionStatus.text = connected ? "Connected" : "Disconnected";
connectionStatus.color = connected ? Color.green : Color.red;
}
private void UpdateTickInfo()
{
if (tickInfo == null) return;
// Format time of day nicely
string timeDisplay = _currentTimeOfDay switch
{
"dawn" => "Dawn",
"day" => "Day",
"dusk" => "Dusk",
"night" => "Night",
_ => "Day"
};
tickInfo.text = $"Day {_currentDay} | {timeDisplay} | {_currentWeather} | Tick {_currentTick} | Alive: {AliveAgentCount}";
}
private void UpdateGoldDisplay()
{
if (goldDisplay == null) return;
goldDisplay.text = $"Gold: {_playerGold}";
}
#endregion
#region Network Event Handlers
private void HandleConnected()
{
Debug.Log("[GameManager] Connected to server");
UpdateConnectionStatus(true);
ShowNotification("Connected to The Island!");
}
private void HandleDisconnected()
{
Debug.Log("[GameManager] Disconnected from server");
UpdateConnectionStatus(false);
ShowNotification("Disconnected from server", isError: true);
}
private void HandleAgentsUpdate(List<AgentData> agentsData)
{
// Null check to prevent exceptions
if (agentsData == null || agentsData.Count == 0)
{
Debug.LogWarning("[GameManager] Received empty agents update");
return;
}
foreach (var data in agentsData)
{
// Check for AgentVisual first (newest system - 2.5D sprites)
if (_agentVisuals.TryGetValue(data.id, out AgentVisual agentVisual))
{
agentVisual.UpdateStats(data);
}
// Check for AgentUI (programmatic UI system)
else if (_agentUIs.TryGetValue(data.id, out AgentUI agentUI))
{
agentUI.UpdateStats(data);
}
// Fallback to AgentController (legacy)
else if (_agents.TryGetValue(data.id, out AgentController controller))
{
controller.UpdateStats(data);
}
else
{
// Spawn new agent
SpawnAgent(data);
}
}
UpdateTickInfo();
}
private void HandleAgentSpeak(AgentSpeakData data)
{
// Check AgentVisual first (newest system - 2.5D sprites)
if (_agentVisuals.TryGetValue(data.agent_id, out AgentVisual agentVisual))
{
agentVisual.ShowSpeech(data.text);
agentVisual.DoJump(); // Add jump effect
}
// Check AgentUI (programmatic UI system)
else if (_agentUIs.TryGetValue(data.agent_id, out AgentUI agentUI))
{
agentUI.ShowSpeech(data.text);
}
// Fallback to AgentController (legacy)
else if (_agents.TryGetValue(data.agent_id, out AgentController controller))
{
controller.ShowSpeech(data.text);
}
else
{
Debug.LogWarning($"[GameManager] Agent {data.agent_id} not found for speech");
}
}
private void HandleAgentDied(AgentDiedData data)
{
Debug.Log($"[GameManager] Agent died: {data.agent_name}");
ShowNotification(data.message, isError: true);
}
private void HandleFeed(FeedEventData data)
{
Debug.Log($"[GameManager] Feed event: {data.message}");
// Update gold if this was our action
if (data.user == NetworkManager.Instance.Username)
{
_playerGold = data.user_gold;
UpdateGoldDisplay();
}
ShowNotification(data.message);
}
private void HandleTick(TickData data)
{
_currentTick = data.tick;
_currentDay = data.day;
// Update weather and time of day from tick data
if (!string.IsNullOrEmpty(data.time_of_day))
{
_currentTimeOfDay = data.time_of_day;
}
if (!string.IsNullOrEmpty(data.weather))
{
_currentWeather = data.weather;
}
UpdateTickInfo();
}
private void HandleSystemMessage(SystemEventData data)
{
Debug.Log($"[GameManager] System: {data.message}");
ShowNotification(data.message);
}
private void HandleUserUpdate(UserUpdateData data)
{
if (data.user == NetworkManager.Instance.Username)
{
_playerGold = data.gold;
UpdateGoldDisplay();
}
}
private void HandleWeatherChange(WeatherChangeData data)
{
Debug.Log($"[GameManager] Weather changed: {data.old_weather} -> {data.new_weather}");
_currentWeather = data.new_weather;
ShowNotification($"Weather: {data.new_weather}");
UpdateTickInfo();
}
private void HandlePhaseChange(PhaseChangeData data)
{
Debug.Log($"[GameManager] Phase changed: {data.old_phase} -> {data.new_phase}");
_currentTimeOfDay = data.new_phase;
ShowNotification($"The {data.new_phase} begins...");
UpdateTickInfo();
}
private void HandleDayChange(DayChangeData data)
{
Debug.Log($"[GameManager] New day: {data.day}");
_currentDay = data.day;
ShowNotification($"Day {data.day} begins!");
UpdateTickInfo();
}
private void HandleHeal(HealEventData data)
{
Debug.Log($"[GameManager] Heal event: {data.message}");
// Update gold if this was our action
if (data.user == NetworkManager.Instance.Username)
{
_playerGold = data.user_gold;
UpdateGoldDisplay();
}
ShowNotification(data.message);
}
private void HandleEncourage(EncourageEventData data)
{
Debug.Log($"[GameManager] Encourage event: {data.message}");
// Update gold if this was our action
if (data.user == NetworkManager.Instance.Username)
{
_playerGold = data.user_gold;
UpdateGoldDisplay();
}
ShowNotification(data.message);
}
private void HandleGiveItem(GiveItemEventData data)
{
Debug.Log($"[GameManager] Give Item: {data.message}");
ShowNotification(data.message);
if (_agentVisuals.TryGetValue(data.from_id, out AgentVisual fromVisual) &&
_agentVisuals.TryGetValue(data.to_id, out AgentVisual toVisual))
{
// Trigger Visual Effect (Phase 23)
// We assume AgentVisual has DoGiveItem method
fromVisual.DoGiveItem(toVisual.transform, data.item_type);
}
}
private void HandleGroupActivity(GroupActivityEventData data)
{
Debug.Log($"[GameManager] Group Activity: {data.activity_type} with {data.storyteller_name}");
if (data.activity_type == "storytelling")
{
ShowNotification($"🔥 {data.storyteller_name} tells a story about {data.topic}...");
// Storyteller Visuals
if (_agentVisuals.TryGetValue(data.storyteller_id, out AgentVisual storyteller))
{
storyteller.ShowSpeech(data.content, 8f); // Long duration for story
storyteller.DoStorytelling(); // Animation trigger
}
// Listener Visuals
if (data.listener_ids != null)
{
foreach (int listenerId in data.listener_ids)
{
if (_agentVisuals.TryGetValue(listenerId, out AgentVisual listener))
{
// Listeners face the storyteller and show interest
if (storyteller != null)
{
listener.DoListen(storyteller.transform);
}
}
}
}
}
}
private void HandleVFXEvent(VFXEventData data)
{
Debug.Log($"[GameManager] VFX: {data.effect}");
if (!string.IsNullOrEmpty(data.message))
{
ShowNotification(data.message);
}
Vector3 targetPos = new Vector3(0, 2, 0); // Default air center
if (data.target_id > 0)
{
if (_agentVisuals.TryGetValue(data.target_id, out AgentVisual visual))
{
targetPos = visual.transform.position;
}
else if (_agentUIs.TryGetValue(data.target_id, out AgentUI ui))
{
targetPos = ui.transform.position;
}
}
if (VFXManager.Instance != null)
{
VFXManager.Instance.PlayEffect(data.effect, targetPos);
}
}
private void HandleTalk(TalkEventData data)
{
Debug.Log($"[GameManager] Talk event: {data.agent_name} responds about '{data.topic}'");
// Show the agent's speech response
if (_agentVisuals.TryGetValue(GetAgentIdByName(data.agent_name), out AgentVisual agentVisual))
{
agentVisual.ShowSpeech(data.response);
}
else if (_agentUIs.TryGetValue(GetAgentIdByName(data.agent_name), out AgentUI agentUI))
{
agentUI.ShowSpeech(data.response);
}
}
private void HandleRevive(ReviveEventData data)
{
Debug.Log($"[GameManager] Revive event: {data.message}");
// Update gold if this was our action
if (data.user == NetworkManager.Instance.Username)
{
_playerGold = data.user_gold;
UpdateGoldDisplay();
}
ShowNotification(data.message);
}
private void HandleSocialInteraction(SocialInteractionData data)
{
Debug.Log($"[GameManager] Social: {data.initiator_name} -> {data.target_name} ({data.interaction_type})");
// Show dialogue from initiator
if (_agentVisuals.TryGetValue(data.initiator_id, out AgentVisual initiatorVisual))
{
initiatorVisual.ShowSpeech(data.dialogue);
}
else if (_agentUIs.TryGetValue(data.initiator_id, out AgentUI initiatorUI))
{
initiatorUI.ShowSpeech(data.dialogue);
}
}
/// <summary>
/// Handle gift effect event (Twitch bits, subscriptions).
/// Plays VFX and shows gratitude speech.
/// </summary>
private void HandleGiftEffect(GiftEffectData data)
{
Debug.Log($"[GameManager] Gift: {data.user} sent {data.value} {data.gift_type}");
// Find target agent position for VFX
Vector3 effectPosition = Vector3.zero;
int agentId = GetAgentIdByName(data.agent_name);
if (agentId >= 0 && _agentVisuals.TryGetValue(agentId, out AgentVisual agentVisual))
{
effectPosition = agentVisual.transform.position;
// Show gratitude speech with extended duration
if (!string.IsNullOrEmpty(data.gratitude))
{
float duration = data.duration > 0 ? data.duration : 8f;
agentVisual.ShowSpeech(data.gratitude, duration);
}
}
else if (agentId >= 0 && _agentUIs.TryGetValue(agentId, out AgentUI agentUI))
{
effectPosition = agentUI.transform.position;
if (!string.IsNullOrEmpty(data.gratitude))
{
agentUI.ShowSpeech(data.gratitude);
}
}
else
{
// Default to center if no agent found
effectPosition = new Vector3(0, 1, 0);
}
// Play VFX
if (VFXManager.Instance != null)
{
VFXManager.Instance.PlayEffect(data.gift_type, effectPosition);
}
// 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))
{
// Phase 21-C: Handle Follow/Target Logic
if (data.action_type == "Follow" && !string.IsNullOrEmpty(data.target_name))
{
int targetId = GetAgentIdByName(data.target_name);
if (targetId >= 0 && _agentVisuals.TryGetValue(targetId, out AgentVisual targetVisual))
{
agentVisual.SetFollowTarget(targetVisual.transform);
Debug.Log($"[GameManager] {data.agent_name} is now following {data.target_name}");
}
}
else
{
// Clear follow target for non-follow actions
agentVisual.SetFollowTarget(null);
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(-12f, 0f, 5.0f); // Phase 20-C.2: Recalibrated
case "tree_right":
return new Vector3(13f, 0f, 5.5f); // Phase 20-C.2: Recalibrated
case "campfire":
case "center":
return new Vector3(0f, 0f, 0f);
case "water":
case "beach":
// Shore starts at Z=7.0. Safe beach area is Z=[3, 6]
return new Vector3(Random.Range(-5, 5), 0f, Random.Range(4, 6));
case "nearby":
// Wandering constrained to dry land Z=[-6, 6]
return new Vector3(Random.Range(-15, 15), 0f, Random.Range(-6, 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
private void SpawnAgent(AgentData data)
{
if (agentPrefab == null)
{
Debug.LogError("[GameManager] Agent prefab not assigned!");
return;
}
// Determine spawn position
Vector3 spawnPos = GetNextSpawnPosition();
// Instantiate prefab
GameObject agentObj = Instantiate(
agentPrefab,
spawnPos,
Quaternion.identity,
agentContainer
);
// Try to get AgentVisual first (newest system - 2.5D sprites)
AgentVisual agentVisual = agentObj.GetComponent<AgentVisual>();
if (agentVisual != null)
{
agentVisual.Initialize(data);
_agentVisuals[data.id] = agentVisual;
Debug.Log($"[GameManager] Spawned agent (AgentVisual): {data.name} at {spawnPos}");
return;
}
// Try to get AgentUI (programmatic UI system)
AgentUI agentUI = agentObj.GetComponent<AgentUI>();
if (agentUI == null)
{
// Add AgentUI component - it will create all UI elements automatically
agentUI = agentObj.AddComponent<AgentUI>();
}
agentUI.Initialize(data);
_agentUIs[data.id] = agentUI;
// Also check for legacy AgentController
AgentController controller = agentObj.GetComponent<AgentController>();
if (controller != null)
{
controller.Initialize(data);
_agents[data.id] = controller;
}
Debug.Log($"[GameManager] Spawned agent: {data.name} at {spawnPos}");
}
private Vector3 GetNextSpawnPosition()
{
if (spawnPositions == null || spawnPositions.Length == 0)
{
return Vector3.zero;
}
Vector3 pos = spawnPositions[_nextSpawnIndex % spawnPositions.Length];
_nextSpawnIndex++;
return pos;
}
/// <summary>
/// Get an agent controller by ID.
/// </summary>
public AgentController GetAgent(int agentId)
{
_agents.TryGetValue(agentId, out AgentController controller);
return controller;
}
/// <summary>
/// Get an agent controller by name.
/// </summary>
public AgentController GetAgentByName(string name)
{
foreach (var agent in _agents.Values)
{
if (agent.CurrentData?.name == name)
{
return agent;
}
}
return null;
}
/// <summary>
/// Get agent ID by name (searches all agent systems).
/// </summary>
private int GetAgentIdByName(string name)
{
// Check AgentVisual first (newest system)
foreach (var kvp in _agentVisuals)
{
if (kvp.Value.CurrentData?.name == name)
{
return kvp.Key;
}
}
// Check AgentUI
foreach (var kvp in _agentUIs)
{
if (kvp.Value.CurrentData?.name == name)
{
return kvp.Key;
}
}
// Check AgentController (legacy)
foreach (var kvp in _agents)
{
if (kvp.Value.CurrentData?.name == name)
{
return kvp.Key;
}
}
return -1;
}
#endregion
#region UI Actions
private void OnResetClicked()
{
NetworkManager.Instance.ResetGame();
ShowNotification("Reset requested...");
}
private void OnSendClicked()
{
SendCommand();
}
private void OnCommandSubmit(string text)
{
SendCommand();
}
private void SendCommand()
{
if (commandInput == null || string.IsNullOrWhiteSpace(commandInput.text))
return;
NetworkManager.Instance.SendCommand(commandInput.text);
commandInput.text = "";
commandInput.ActivateInputField();
}
/// <summary>
/// Feed a specific agent by name.
/// Called from UI buttons.
/// </summary>
public void FeedAgent(string agentName)
{
NetworkManager.Instance.FeedAgent(agentName);
}
#endregion
#region Notifications
private void ShowNotification(string message, bool isError = false)
{
if (notificationPanel == null || notificationText == null)
return;
notificationText.text = message;
notificationText.color = isError ? Color.red : Color.white;
notificationPanel.SetActive(true);
// Auto-hide
CancelInvoke(nameof(HideNotification));
Invoke(nameof(HideNotification), notificationDuration);
}
private void HideNotification()
{
if (notificationPanel != null)
{
notificationPanel.SetActive(false);
}
}
#endregion
}
}